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
|
cd /github/workspace
|
||||||
npm ci
|
npm ci
|
||||||
cd desktop
|
|
||||||
npm ci
|
|
||||||
cd /github/workspace
|
|
||||||
grunt desktop-linux
|
grunt desktop-linux
|
||||||
|
|
|
@ -24,6 +24,11 @@ jobs:
|
||||||
run: npm test
|
run: npm test
|
||||||
- name: Grunt
|
- name: Grunt
|
||||||
run: grunt
|
run: grunt
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
||||||
|
path: dist
|
||||||
- name: Write secrets
|
- name: Write secrets
|
||||||
env:
|
env:
|
||||||
VIRUS_TOTAL: ${{ secrets.VIRUS_TOTAL }}
|
VIRUS_TOTAL: ${{ secrets.VIRUS_TOTAL }}
|
||||||
|
@ -33,11 +38,6 @@ jobs:
|
||||||
- name: Check on VirusTotal
|
- name: Check on VirusTotal
|
||||||
run: grunt virustotal
|
run: grunt virustotal
|
||||||
if: ${{ github.repository == 'keeweb/keeweb' }}
|
if: ${{ github.repository == 'keeweb/keeweb' }}
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v1
|
|
||||||
with:
|
|
||||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
|
|
||||||
path: dist
|
|
||||||
|
|
||||||
linux:
|
linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -95,11 +95,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
||||||
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
|
||||||
- name: Upload update artifact
|
|
||||||
uses: actions/upload-artifact@v1
|
|
||||||
with:
|
|
||||||
name: UpdateDesktop.zip
|
|
||||||
path: dist/desktop/UpdateDesktop.zip
|
|
||||||
|
|
||||||
darwin:
|
darwin:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
@ -124,9 +119,6 @@ jobs:
|
||||||
path: dist
|
path: dist
|
||||||
- name: Install npm modules
|
- name: Install npm modules
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Install desktop npm modules
|
|
||||||
working-directory: desktop
|
|
||||||
run: npm ci
|
|
||||||
- name: Install grunt
|
- name: Install grunt
|
||||||
run: sudo npm i -g grunt-cli
|
run: sudo npm i -g grunt-cli
|
||||||
- name: Write secrets
|
- name: Write secrets
|
||||||
|
@ -134,10 +126,12 @@ jobs:
|
||||||
CODESIGN: ${{ secrets.CODESIGN }}
|
CODESIGN: ${{ secrets.CODESIGN }}
|
||||||
APPLE_DEPLOY_PASSWORD: ${{ secrets.APPLE_DEPLOY_PASSWORD }}
|
APPLE_DEPLOY_PASSWORD: ${{ secrets.APPLE_DEPLOY_PASSWORD }}
|
||||||
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
||||||
|
APPLE_PROVISIONING_PROFILE: ${{ secrets.APPLE_PROVISIONING_PROFILE }}
|
||||||
run: |
|
run: |
|
||||||
mkdir keys
|
mkdir keys
|
||||||
echo "$CODESIGN" > keys/codesign.json
|
echo "$CODESIGN" > keys/codesign.json
|
||||||
xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "$APPLE_ID_USERNAME" -p "$APPLE_DEPLOY_PASSWORD"
|
xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "$APPLE_ID_USERNAME" -p "$APPLE_DEPLOY_PASSWORD"
|
||||||
|
echo "$APPLE_PROVISIONING_PROFILE" | base64 -d > keys/keeweb.provisionprofile
|
||||||
- name: Import certificates
|
- name: Import certificates
|
||||||
uses: keeweb/import-codesign-certs@v1
|
uses: keeweb/import-codesign-certs@v1
|
||||||
with:
|
with:
|
||||||
|
@ -179,9 +173,6 @@ jobs:
|
||||||
path: dist
|
path: dist
|
||||||
- name: Install npm modules
|
- name: Install npm modules
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Install desktop npm modules
|
|
||||||
working-directory: desktop
|
|
||||||
run: npm ci
|
|
||||||
- name: Install grunt
|
- name: Install grunt
|
||||||
run: npm i -g grunt-cli
|
run: npm i -g grunt-cli
|
||||||
- name: Write secrets
|
- name: Write secrets
|
||||||
|
@ -328,11 +319,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||||
path: assets
|
path: assets
|
||||||
- name: Download update artifact
|
|
||||||
uses: actions/download-artifact@v1
|
|
||||||
with:
|
|
||||||
name: UpdateDesktop.zip
|
|
||||||
path: assets
|
|
||||||
- name: Zip html
|
- name: Zip html
|
||||||
working-directory: html
|
working-directory: html
|
||||||
run: zip -vr ../assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip .
|
run: zip -vr ../assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip .
|
||||||
|
@ -505,15 +491,6 @@ jobs:
|
||||||
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||||
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
|
||||||
asset_content_type: application/octet-stream
|
asset_content_type: application/octet-stream
|
||||||
- name: Upload update asset
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: assets/UpdateDesktop.zip
|
|
||||||
asset_name: UpdateDesktop.zip
|
|
||||||
asset_content_type: application/octet-stream
|
|
||||||
- name: Upload verify.sign asset
|
- name: Upload verify.sign asset
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
|
|
143
Gruntfile.js
143
Gruntfile.js
|
@ -25,16 +25,24 @@ module.exports = function (grunt) {
|
||||||
|
|
||||||
const dt = date.toISOString().replace(/T.*/, '');
|
const dt = date.toISOString().replace(/T.*/, '');
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const minElectronVersionForUpdate = '11.0.3';
|
|
||||||
const zipCommentPlaceholderPart = 'zip_comment_placeholder_that_will_be_replaced_with_hash';
|
|
||||||
const zipCommentPlaceholder =
|
|
||||||
zipCommentPlaceholderPart + '.'.repeat(512 - zipCommentPlaceholderPart.length);
|
|
||||||
const electronVersion = pkg.dependencies.electron.replace(/^\D/, '');
|
const electronVersion = pkg.dependencies.electron.replace(/^\D/, '');
|
||||||
const skipSign = grunt.option('skip-sign');
|
const skipSign = grunt.option('skip-sign');
|
||||||
const getCodeSignConfig = () =>
|
const getCodeSignConfig = () =>
|
||||||
skipSign ? { identities: {} } : require('./keys/codesign.json');
|
skipSign ? { identities: {} } : require('./keys/codesign.json');
|
||||||
|
|
||||||
const sha = execSync('git rev-parse --short HEAD').toString('utf8').trim();
|
let sha = grunt.option('commit-sha');
|
||||||
|
if (!sha) {
|
||||||
|
try {
|
||||||
|
sha = execSync('git rev-parse --short HEAD').toString('utf8').trim();
|
||||||
|
} catch (e) {
|
||||||
|
grunt.warn(
|
||||||
|
"Cannot get commit sha from git. It's recommended to build KeeWeb from a git repo " +
|
||||||
|
'because commit sha is displayed in the UI, however if you would like to build from a folder, ' +
|
||||||
|
'you can override what will be displayed in the UI with --commit-sha=xxx.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
grunt.log.writeln(`Building KeeWeb v${pkg.version} (${sha})`);
|
||||||
|
|
||||||
const webpackOptions = {
|
const webpackOptions = {
|
||||||
date,
|
date,
|
||||||
|
@ -68,6 +76,15 @@ module.exports = function (grunt) {
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const linuxDependencies = [
|
||||||
|
'libappindicator1',
|
||||||
|
'libgconf-2-4',
|
||||||
|
'gnome-keyring',
|
||||||
|
'libxtst6',
|
||||||
|
'libx11-6',
|
||||||
|
'libatspi2.0-0'
|
||||||
|
];
|
||||||
|
|
||||||
grunt.initConfig({
|
grunt.initConfig({
|
||||||
noop: { noop: {} },
|
noop: { noop: {} },
|
||||||
clean: {
|
clean: {
|
||||||
|
@ -127,18 +144,6 @@ module.exports = function (grunt) {
|
||||||
expand: true,
|
expand: true,
|
||||||
nonull: true
|
nonull: true
|
||||||
},
|
},
|
||||||
'desktop-update': {
|
|
||||||
cwd: 'tmp/desktop/keeweb-linux-x64/resources/',
|
|
||||||
src: 'app.asar',
|
|
||||||
dest: 'tmp/desktop/update/',
|
|
||||||
expand: true,
|
|
||||||
nonull: true
|
|
||||||
},
|
|
||||||
'desktop-update-helper': {
|
|
||||||
src: ['helper/darwin/KeeWebHelper', 'helper/win32/KeeWebHelper.exe'],
|
|
||||||
dest: 'tmp/desktop/update/',
|
|
||||||
nonull: true
|
|
||||||
},
|
|
||||||
'desktop-darwin-helper-x64': {
|
'desktop-darwin-helper-x64': {
|
||||||
src: 'helper/darwin/KeeWebHelper',
|
src: 'helper/darwin/KeeWebHelper',
|
||||||
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/',
|
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/',
|
||||||
|
@ -152,7 +157,7 @@ module.exports = function (grunt) {
|
||||||
options: { mode: '0755' }
|
options: { mode: '0755' }
|
||||||
},
|
},
|
||||||
'desktop-darwin-installer-helper-x64': {
|
'desktop-darwin-installer-helper-x64': {
|
||||||
cwd: 'package/osx/KeeWeb Installer.app',
|
cwd: 'tmp/desktop/KeeWeb Installer.app',
|
||||||
src: '**',
|
src: '**',
|
||||||
dest:
|
dest:
|
||||||
'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
|
'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
|
||||||
|
@ -161,7 +166,7 @@ module.exports = function (grunt) {
|
||||||
options: { mode: true }
|
options: { mode: true }
|
||||||
},
|
},
|
||||||
'desktop-darwin-installer-helper-arm64': {
|
'desktop-darwin-installer-helper-arm64': {
|
||||||
cwd: 'package/osx/KeeWeb Installer.app',
|
cwd: 'tmp/desktop/KeeWeb Installer.app',
|
||||||
src: '**',
|
src: '**',
|
||||||
dest:
|
dest:
|
||||||
'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
|
'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
|
||||||
|
@ -243,6 +248,11 @@ module.exports = function (grunt) {
|
||||||
src: `tmp/desktop/electron-builder/keeweb-${pkg.version}.AppImage`,
|
src: `tmp/desktop/electron-builder/keeweb-${pkg.version}.AppImage`,
|
||||||
dest: `dist/desktop/KeeWeb-${pkg.version}.linux.AppImage`,
|
dest: `dist/desktop/KeeWeb-${pkg.version}.linux.AppImage`,
|
||||||
nonull: true
|
nonull: true
|
||||||
|
},
|
||||||
|
'darwin-installer-icon': {
|
||||||
|
src: 'graphics/icon.icns',
|
||||||
|
dest: 'tmp/desktop/KeeWeb Installer.app/Contents/Resources/applet.icns',
|
||||||
|
nonull: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
eslint: {
|
eslint: {
|
||||||
|
@ -250,7 +260,8 @@ module.exports = function (grunt) {
|
||||||
desktop: ['desktop/**/*.js', '!desktop/node_modules/**'],
|
desktop: ['desktop/**/*.js', '!desktop/node_modules/**'],
|
||||||
build: ['Gruntfile.js', 'grunt.*.js', 'build/**/*.js', 'webpack.config.js'],
|
build: ['Gruntfile.js', 'grunt.*.js', 'build/**/*.js', 'webpack.config.js'],
|
||||||
plugins: ['plugins/**/*.js'],
|
plugins: ['plugins/**/*.js'],
|
||||||
util: ['util/**/*.js']
|
util: ['util/**/*.js'],
|
||||||
|
installer: ['package/osx/installer.js']
|
||||||
},
|
},
|
||||||
inline: {
|
inline: {
|
||||||
app: {
|
app: {
|
||||||
|
@ -263,7 +274,7 @@ module.exports = function (grunt) {
|
||||||
algo: 'sha512',
|
algo: 'sha512',
|
||||||
expected: {
|
expected: {
|
||||||
style: 1,
|
style: 1,
|
||||||
script: 2
|
script: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
@ -283,20 +294,20 @@ module.exports = function (grunt) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'string-replace': {
|
'string-replace': {
|
||||||
manifest: {
|
'update-manifest': {
|
||||||
options: {
|
options: {
|
||||||
replacements: [
|
replacements: [
|
||||||
{
|
{
|
||||||
pattern: '# YYYY-MM-DD:v0.0.0',
|
pattern: /"version":\s*".*?"/,
|
||||||
replacement: '# ' + dt + ':v' + pkg.version
|
replacement: `"version": "${pkg.version}"`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pattern: '# updmin:v0.0.0',
|
pattern: /"date":\s*".*?"/,
|
||||||
replacement: '# updmin:v' + minElectronVersionForUpdate
|
replacement: `"date": "${dt}"`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
files: { 'dist/manifest.appcache': 'app/manifest.appcache' }
|
files: { 'dist/update.json': 'app/update.json' }
|
||||||
},
|
},
|
||||||
'service-worker': {
|
'service-worker': {
|
||||||
options: { replacements: [{ pattern: '0.0.0', replacement: pkg.version }] },
|
options: { replacements: [{ pattern: '0.0.0', replacement: pkg.version }] },
|
||||||
|
@ -437,26 +448,37 @@ module.exports = function (grunt) {
|
||||||
category: 'Utility'
|
category: 'Utility'
|
||||||
},
|
},
|
||||||
rpm: {
|
rpm: {
|
||||||
// depends: ['libappindicator1', 'libgconf-2-4', 'gnome-keyring']
|
// depends: linuxDependencies
|
||||||
},
|
},
|
||||||
snap: {
|
snap: {
|
||||||
stagePackages: ['libappindicator1', 'libgconf-2-4', 'gnome-keyring']
|
stagePackages: linuxDependencies
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'electron-patch': {
|
||||||
|
'win32-x64': 'tmp/desktop/KeeWeb-win32-x64/KeeWeb.exe',
|
||||||
|
'win32-ia32': 'tmp/desktop/KeeWeb-win32-ia32/KeeWeb.exe',
|
||||||
|
'win32-arm64': 'tmp/desktop/KeeWeb-win32-arm64/KeeWeb.exe',
|
||||||
|
'darwin-x64': 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app',
|
||||||
|
'darwin-arm64': 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app',
|
||||||
|
'linux': 'tmp/desktop/KeeWeb-linux-x64/keeweb'
|
||||||
|
},
|
||||||
|
osacompile: {
|
||||||
|
options: {
|
||||||
|
language: 'JavaScript'
|
||||||
|
},
|
||||||
|
installer: {
|
||||||
|
files: {
|
||||||
|
'tmp/desktop/KeeWeb Installer.app': 'package/osx/installer.js'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
compress: {
|
compress: {
|
||||||
options: {
|
options: {
|
||||||
level: 6
|
level: 6
|
||||||
},
|
},
|
||||||
'desktop-update': {
|
|
||||||
options: {
|
|
||||||
archive: 'dist/desktop/UpdateDesktop.zip',
|
|
||||||
comment: zipCommentPlaceholder
|
|
||||||
},
|
|
||||||
files: [{ cwd: 'tmp/desktop/update', src: '**', expand: true, nonull: true }]
|
|
||||||
},
|
|
||||||
'win32-x64': {
|
'win32-x64': {
|
||||||
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.x64.zip` },
|
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.x64.zip` },
|
||||||
files: [{ cwd: 'tmp/desktop/KeeWeb-win32-x64', src: '**', expand: true }]
|
files: [{ cwd: 'tmp/desktop/KeeWeb-win32-x64', src: '**', expand: true }]
|
||||||
|
@ -565,7 +587,7 @@ module.exports = function (grunt) {
|
||||||
pkgName: `KeeWeb-${pkg.version}.linux.x64.deb`,
|
pkgName: `KeeWeb-${pkg.version}.linux.x64.deb`,
|
||||||
targetDir: 'dist/desktop',
|
targetDir: 'dist/desktop',
|
||||||
appName: 'KeeWeb',
|
appName: 'KeeWeb',
|
||||||
depends: 'libappindicator1, libgconf-2-4, gnome-keyring',
|
depends: linuxDependencies.join(', '),
|
||||||
scripts: {
|
scripts: {
|
||||||
postinst: 'package/deb/scripts/postinst'
|
postinst: 'package/deb/scripts/postinst'
|
||||||
}
|
}
|
||||||
|
@ -588,50 +610,30 @@ module.exports = function (grunt) {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'sign-archive': {
|
|
||||||
'desktop-update': {
|
|
||||||
options: {
|
|
||||||
file: 'dist/desktop/UpdateDesktop.zip',
|
|
||||||
signature: zipCommentPlaceholder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'sign-desktop-files': {
|
|
||||||
'desktop-update': {
|
|
||||||
options: {
|
|
||||||
path: 'tmp/desktop/update'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'validate-desktop-update': {
|
|
||||||
desktop: {
|
|
||||||
options: {
|
|
||||||
file: 'dist/desktop/UpdateDesktop.zip',
|
|
||||||
expected: [
|
|
||||||
'app.asar',
|
|
||||||
'helper/darwin/KeeWebHelper',
|
|
||||||
'helper/win32/KeeWebHelper.exe'
|
|
||||||
],
|
|
||||||
expectedCount: 7,
|
|
||||||
publicKey: 'app/resources/public-key.pem'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'osx-sign': {
|
'osx-sign': {
|
||||||
options: {
|
options: {
|
||||||
get identity() {
|
get identity() {
|
||||||
return getCodeSignConfig().identities.app;
|
return getCodeSignConfig().identities.app;
|
||||||
},
|
},
|
||||||
hardenedRuntime: true,
|
hardenedRuntime: true,
|
||||||
entitlements: 'package/osx/entitlements.mac.plist',
|
entitlements: 'package/osx/entitlements.plist',
|
||||||
'entitlements-inherit': 'package/osx/entitlements.mac.plist',
|
'entitlements-inherit': 'package/osx/entitlements-inherit.plist',
|
||||||
'gatekeeper-assess': false
|
'gatekeeper-assess': false
|
||||||
},
|
},
|
||||||
'desktop-x64': {
|
'desktop-x64': {
|
||||||
|
options: {
|
||||||
|
'provisioning-profile': './keys/keeweb.provisionprofile'
|
||||||
|
},
|
||||||
src: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app'
|
src: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app'
|
||||||
},
|
},
|
||||||
'desktop-arm64': {
|
'desktop-arm64': {
|
||||||
|
options: {
|
||||||
|
'provisioning-profile': './keys/keeweb.provisionprofile'
|
||||||
|
},
|
||||||
src: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app'
|
src: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app'
|
||||||
|
},
|
||||||
|
'installer': {
|
||||||
|
src: 'tmp/desktop/KeeWeb Installer.app'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
notarize: {
|
notarize: {
|
||||||
|
@ -747,10 +749,7 @@ module.exports = function (grunt) {
|
||||||
sign: 'dist/desktop/Verify.sign.sha256'
|
sign: 'dist/desktop/Verify.sign.sha256'
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
'dist/desktop/Verify.sha256': [
|
'dist/desktop/Verify.sha256': ['dist/desktop/KeeWeb-*']
|
||||||
'dist/desktop/KeeWeb-*',
|
|
||||||
'dist/desktop/UpdateDesktop.zip'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2020 Antelle https://antelle.net
|
Copyright (c) 2021 Antelle https://antelle.net
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
@ -90,7 +90,7 @@ Please note: donation does not imply any type of service contract.
|
||||||
|
|
||||||
Notable contributions to KeeWeb:
|
Notable contributions to KeeWeb:
|
||||||
|
|
||||||
- Florian Reuschel ([@Loilo](https://github.com/Loilo)): [German translation](http://keeweb.oneskyapp.com/collaboration/translate/project/project/173183/language/550)
|
- Florian Reuschel ([@Loilo](https://github.com/Loilo)): [German translation](https://keeweb.oneskyapp.com/collaboration/translate/project/project/173183/language/550)
|
||||||
- Dennis Ploeger ([@dploeger](https://github.com/dploeger)): [auto-type improvements](https://github.com/keeweb/keeweb/pulls?q=is%3Apr+is%3Aclosed+author%3Adploeger)
|
- Dennis Ploeger ([@dploeger](https://github.com/dploeger)): [auto-type improvements](https://github.com/keeweb/keeweb/pulls?q=is%3Apr+is%3Aclosed+author%3Adploeger)
|
||||||
- Hackmanit ([hackmanit.de](https://www.hackmanit.de)): [penetration test](https://www.hackmanit.de/en/blog-en/104-pro-bono-penetration-test-keeweb)
|
- Hackmanit ([hackmanit.de](https://www.hackmanit.de)): [penetration test](https://www.hackmanit.de/en/blog-en/104-pro-bono-penetration-test-keeweb)
|
||||||
- Peter Bittner ([@bittner](https://github.com/bittner)): [Wikipedia article](https://en.wikipedia.org/wiki/KeeWeb)
|
- Peter Bittner ([@bittner](https://github.com/bittner)): [Wikipedia article](https://en.wikipedia.org/wiki/KeeWeb)
|
||||||
|
|
|
@ -72,7 +72,6 @@
|
||||||
<link rel="manifest" href="manifest.json" />
|
<link rel="manifest" href="manifest.json" />
|
||||||
<link rel="stylesheet" href="css/app.css?__inline=true" />
|
<link rel="stylesheet" href="css/app.css?__inline=true" />
|
||||||
<script src="js/app.js?__inline=true"></script>
|
<script src="js/app.js?__inline=true"></script>
|
||||||
<script src="js/runtime.js?__inline=true"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="th-d">
|
<body class="th-d">
|
||||||
<noscript>
|
<noscript>
|
||||||
|
|
|
@ -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 { FeatureTester } from 'comp/browser/feature-tester';
|
||||||
import { FocusDetector } from 'comp/browser/focus-detector';
|
import { FocusDetector } from 'comp/browser/focus-detector';
|
||||||
import { IdleTracker } from 'comp/browser/idle-tracker';
|
import { IdleTracker } from 'comp/browser/idle-tracker';
|
||||||
|
import { ThemeWatcher } from 'comp/browser/theme-watcher';
|
||||||
import { KeyHandler } from 'comp/browser/key-handler';
|
import { KeyHandler } from 'comp/browser/key-handler';
|
||||||
import { PopupNotifier } from 'comp/browser/popup-notifier';
|
import { PopupNotifier } from 'comp/browser/popup-notifier';
|
||||||
import { Launcher } from 'comp/launcher';
|
import { Launcher } from 'comp/launcher';
|
||||||
|
@ -91,6 +92,8 @@ ready(() => {
|
||||||
KdbxwebInit.init();
|
KdbxwebInit.init();
|
||||||
FocusDetector.init();
|
FocusDetector.init();
|
||||||
AutoType.init();
|
AutoType.init();
|
||||||
|
ThemeWatcher.init();
|
||||||
|
SettingsManager.init();
|
||||||
window.kw = ExportApi;
|
window.kw = ExportApi;
|
||||||
return PluginManager.init().then(() => {
|
return PluginManager.init().then(() => {
|
||||||
StartProfiler.milestone('initializing modules');
|
StartProfiler.milestone('initializing modules');
|
||||||
|
@ -111,13 +114,13 @@ ready(() => {
|
||||||
function loadRemoteConfig() {
|
function loadRemoteConfig() {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
SettingsManager.setBySettings(appModel.settings);
|
SettingsManager.setBySettings();
|
||||||
const configParam = getConfigParam();
|
const configParam = getConfigParam();
|
||||||
if (configParam) {
|
if (configParam) {
|
||||||
return appModel
|
return appModel
|
||||||
.loadConfig(configParam)
|
.loadConfig(configParam)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
SettingsManager.setBySettings(appModel.settings);
|
SettingsManager.setBySettings();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
if (!appModel.settings.cacheConfigSettings) {
|
if (!appModel.settings.cacheConfigSettings) {
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import { Launcher } from 'comp/launcher';
|
import { Launcher } from 'comp/launcher';
|
||||||
|
import { AppSettingsModel } from 'models/app-settings-model';
|
||||||
|
import { AutoTypeEmitter } from 'auto-type/auto-type-emitter';
|
||||||
|
|
||||||
const AutoTypeEmitterFactory = {
|
const AutoTypeEmitterFactory = {
|
||||||
create(callback, windowId) {
|
create(callback, windowId) {
|
||||||
if (Launcher && Launcher.autoTypeSupported) {
|
if (Launcher && Launcher.autoTypeSupported) {
|
||||||
const { AutoTypeEmitter } = require('./emitter/auto-type-emitter-' +
|
if (AppSettingsModel.useLegacyAutoType) {
|
||||||
Launcher.platform());
|
const { AutoTypeEmitter } = require('./emitter/auto-type-emitter-' +
|
||||||
|
Launcher.platform());
|
||||||
|
return new AutoTypeEmitter(callback, windowId);
|
||||||
|
}
|
||||||
return new AutoTypeEmitter(callback, windowId);
|
return new AutoTypeEmitter(callback, windowId);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -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 { Launcher } from 'comp/launcher';
|
||||||
|
import { AppSettingsModel } from 'models/app-settings-model';
|
||||||
|
import { AutoTypeHelper } from 'auto-type/auto-type-helper';
|
||||||
|
|
||||||
const AutoTypeHelperFactory = {
|
const AutoTypeHelperFactory = {
|
||||||
create() {
|
create() {
|
||||||
if (Launcher && Launcher.autoTypeSupported) {
|
if (Launcher && Launcher.autoTypeSupported) {
|
||||||
const { AutoTypeHelper } = require('./helper/auto-type-helper-' + Launcher.platform());
|
if (AppSettingsModel.useLegacyAutoType) {
|
||||||
|
const { AutoTypeHelper } = require('./helper/auto-type-helper-' +
|
||||||
|
Launcher.platform());
|
||||||
|
return new AutoTypeHelper();
|
||||||
|
}
|
||||||
return new AutoTypeHelper();
|
return new AutoTypeHelper();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -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 { AutoTypeObfuscator } from 'auto-type/auto-type-obfuscator';
|
||||||
import { StringFormat } from 'util/formatting/string-format';
|
import { StringFormat } from 'util/formatting/string-format';
|
||||||
import { Logger } from 'util/logger';
|
import { Logger } from 'util/logger';
|
||||||
|
import { AppSettingsModel } from 'models/app-settings-model';
|
||||||
|
|
||||||
const emitterLogger = new Logger(
|
const emitterLogger = new Logger(
|
||||||
'auto-type-emitter',
|
'auto-type-emitter',
|
||||||
undefined,
|
undefined,
|
||||||
localStorage.debugAutoType ? Logger.Level.All : Logger.Level.Warn
|
localStorage.debugAutoType ? Logger.Level.All : Logger.Level.Info
|
||||||
);
|
);
|
||||||
|
|
||||||
const AutoTypeRunner = function (ops) {
|
const AutoTypeRunner = function (ops) {
|
||||||
|
@ -239,7 +240,7 @@ AutoTypeRunner.prototype.tryParseCommand = function (op) {
|
||||||
// {VKEY 10} {VKEY 0x1F}
|
// {VKEY 10} {VKEY 0x1F}
|
||||||
op.type = 'key';
|
op.type = 'key';
|
||||||
op.value = parseInt(op.arg);
|
op.value = parseInt(op.arg);
|
||||||
if (isNaN(op.value) || op.value <= 0) {
|
if (isNaN(op.value) || op.value < 0) {
|
||||||
throw 'Bad vkey: ' + op.arg;
|
throw 'Bad vkey: ' + op.arg;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -432,6 +433,8 @@ AutoTypeRunner.prototype.obfuscateOp = function (op) {
|
||||||
};
|
};
|
||||||
|
|
||||||
AutoTypeRunner.prototype.run = function (callback, windowId) {
|
AutoTypeRunner.prototype.run = function (callback, windowId) {
|
||||||
|
const emitterType = AppSettingsModel.useLegacyAutoType ? 'legacy' : 'native';
|
||||||
|
emitterLogger.info(`Using ${emitterType} auto-type emitter`);
|
||||||
this.emitter = AutoTypeEmitterFactory.create(this.emitNext.bind(this), windowId);
|
this.emitter = AutoTypeEmitterFactory.create(this.emitNext.bind(this), windowId);
|
||||||
this.emitterState = {
|
this.emitterState = {
|
||||||
callback,
|
callback,
|
||||||
|
@ -442,7 +445,7 @@ AutoTypeRunner.prototype.run = function (callback, windowId) {
|
||||||
activeMod: {},
|
activeMod: {},
|
||||||
finished: null
|
finished: null
|
||||||
};
|
};
|
||||||
this.emitNext();
|
this.emitter.begin();
|
||||||
};
|
};
|
||||||
|
|
||||||
AutoTypeRunner.prototype.emitNext = function (err) {
|
AutoTypeRunner.prototype.emitNext = function (err) {
|
||||||
|
|
|
@ -65,6 +65,10 @@ const AutoTypeEmitter = function (callback) {
|
||||||
this.pendingScript = [];
|
this.pendingScript = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AutoTypeEmitter.prototype.begin = function () {
|
||||||
|
this.callback();
|
||||||
|
};
|
||||||
|
|
||||||
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
|
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
this.mod[ModMap[mod]] = true;
|
this.mod[ModMap[mod]] = true;
|
||||||
|
|
|
@ -68,6 +68,10 @@ const AutoTypeEmitter = function (callback, windowId) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AutoTypeEmitter.prototype.begin = function () {
|
||||||
|
this.callback();
|
||||||
|
};
|
||||||
|
|
||||||
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
|
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
this.mod[ModMap[mod]] = true;
|
this.mod[ModMap[mod]] = true;
|
||||||
|
|
|
@ -65,6 +65,10 @@ const AutoTypeEmitter = function (callback) {
|
||||||
this.pendingScript = [];
|
this.pendingScript = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AutoTypeEmitter.prototype.begin = function () {
|
||||||
|
this.callback();
|
||||||
|
};
|
||||||
|
|
||||||
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
|
AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
this.mod[ModMap[mod]] = true;
|
this.mod[ModMap[mod]] = true;
|
||||||
|
|
|
@ -3,19 +3,20 @@ import { AutoTypeFilter } from 'auto-type/auto-type-filter';
|
||||||
import { AutoTypeHelperFactory } from 'auto-type/auto-type-helper-factory';
|
import { AutoTypeHelperFactory } from 'auto-type/auto-type-helper-factory';
|
||||||
import { AutoTypeParser } from 'auto-type/auto-type-parser';
|
import { AutoTypeParser } from 'auto-type/auto-type-parser';
|
||||||
import { Launcher } from 'comp/launcher';
|
import { Launcher } from 'comp/launcher';
|
||||||
|
import { Features } from 'util/features';
|
||||||
import { Alerts } from 'comp/ui/alerts';
|
import { Alerts } from 'comp/ui/alerts';
|
||||||
import { Timeouts } from 'const/timeouts';
|
import { Timeouts } from 'const/timeouts';
|
||||||
import { AppSettingsModel } from 'models/app-settings-model';
|
import { AppSettingsModel } from 'models/app-settings-model';
|
||||||
import { AppModel } from 'models/app-model';
|
import { AppModel } from 'models/app-model';
|
||||||
import { Locale } from 'util/locale';
|
import { Locale } from 'util/locale';
|
||||||
import { Logger } from 'util/logger';
|
import { Logger } from 'util/logger';
|
||||||
|
import { Links } from 'const/links';
|
||||||
import { AutoTypeSelectView } from 'views/auto-type/auto-type-select-view';
|
import { AutoTypeSelectView } from 'views/auto-type/auto-type-select-view';
|
||||||
|
|
||||||
const logger = new Logger('auto-type');
|
const logger = new Logger('auto-type');
|
||||||
const clearTextAutoTypeLog = !!localStorage.debugAutoType;
|
const clearTextAutoTypeLog = !!localStorage.debugAutoType;
|
||||||
|
|
||||||
const AutoType = {
|
const AutoType = {
|
||||||
helper: AutoTypeHelperFactory.create(),
|
|
||||||
enabled: !!(Launcher && Launcher.autoTypeSupported),
|
enabled: !!(Launcher && Launcher.autoTypeSupported),
|
||||||
supportsEventsWithWindowId: !!(Launcher && Launcher.platform() === 'linux'),
|
supportsEventsWithWindowId: !!(Launcher && Launcher.platform() === 'linux'),
|
||||||
selectEntryView: false,
|
selectEntryView: false,
|
||||||
|
@ -64,9 +65,16 @@ const AutoType = {
|
||||||
runAndHandleResult(result, windowId) {
|
runAndHandleResult(result, windowId) {
|
||||||
this.run(result, windowId, (err) => {
|
this.run(result, windowId, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
let body = Locale.autoTypeErrorGeneric.replace('{}', err.message || err.toString());
|
||||||
|
let link;
|
||||||
|
if (err.keyPressFailed && Features.isMac) {
|
||||||
|
body = Locale.autoTypeErrorAccessibilityMacOS;
|
||||||
|
link = Links.AutoTypeMacOS;
|
||||||
|
}
|
||||||
Alerts.error({
|
Alerts.error({
|
||||||
header: Locale.autoTypeError,
|
header: Locale.autoTypeError,
|
||||||
body: Locale.autoTypeErrorGeneric.replace('{}', err.toString())
|
body,
|
||||||
|
link
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -160,8 +168,10 @@ const AutoType = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getActiveWindowInfo(callback) {
|
getActiveWindowInfo(callback) {
|
||||||
logger.debug('Getting window info');
|
const helperType = AppSettingsModel.useLegacyAutoType ? 'legacy' : 'native';
|
||||||
return this.helper.getActiveWindowInfo((err, windowInfo) => {
|
logger.debug(`Getting window info using ${helperType} helper`);
|
||||||
|
const helper = AutoTypeHelperFactory.create();
|
||||||
|
return helper.getActiveWindowInfo((err, windowInfo) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.error('Error getting window info', err);
|
logger.error('Error getting window info', err);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -58,6 +58,7 @@ const AppRightsChecker = {
|
||||||
runInstaller() {
|
runInstaller() {
|
||||||
Launcher.spawn({
|
Launcher.spawn({
|
||||||
cmd: this.AppPath + '/Contents/Installer/KeeWeb Installer.app/Contents/MacOS/applet',
|
cmd: this.AppPath + '/Contents/Installer/KeeWeb Installer.app/Contents/MacOS/applet',
|
||||||
|
args: ['--install'],
|
||||||
complete: () => {
|
complete: () => {
|
||||||
this.needRunInstaller((needRun) => {
|
this.needRunInstaller((needRun) => {
|
||||||
if (this.alert && !needRun) {
|
if (this.alert && !needRun) {
|
||||||
|
|
|
@ -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 { Events } from 'framework/events';
|
||||||
import { RuntimeInfo } from 'const/runtime-info';
|
import { RuntimeInfo } from 'const/runtime-info';
|
||||||
import { Transport } from 'comp/browser/transport';
|
import { Transport } from 'comp/browser/transport';
|
||||||
|
@ -15,10 +16,9 @@ const Updater = {
|
||||||
UpdateInterval: 1000 * 60 * 60 * 24,
|
UpdateInterval: 1000 * 60 * 60 * 24,
|
||||||
MinUpdateTimeout: 500,
|
MinUpdateTimeout: 500,
|
||||||
MinUpdateSize: 10000,
|
MinUpdateSize: 10000,
|
||||||
UpdateCheckFiles: ['app.asar'],
|
|
||||||
nextCheckTimeout: null,
|
nextCheckTimeout: null,
|
||||||
updateCheckDate: new Date(0),
|
updateCheckDate: new Date(0),
|
||||||
enabled: Launcher && Launcher.updaterEnabled(),
|
enabled: Launcher?.updaterEnabled(),
|
||||||
|
|
||||||
getAutoUpdateType() {
|
getAutoUpdateType() {
|
||||||
if (!this.enabled) {
|
if (!this.enabled) {
|
||||||
|
@ -34,7 +34,7 @@ const Updater = {
|
||||||
updateInProgress() {
|
updateInProgress() {
|
||||||
return (
|
return (
|
||||||
UpdateModel.status === 'checking' ||
|
UpdateModel.status === 'checking' ||
|
||||||
['downloading', 'extracting'].indexOf(UpdateModel.updateStatus) >= 0
|
['downloading', 'extracting', 'updating'].indexOf(UpdateModel.updateStatus) >= 0
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -97,13 +97,12 @@ const Updater = {
|
||||||
}
|
}
|
||||||
logger.info('Checking for update...');
|
logger.info('Checking for update...');
|
||||||
Transport.httpGet({
|
Transport.httpGet({
|
||||||
url: Links.Manifest,
|
url: Links.UpdateJson,
|
||||||
utf8: true,
|
json: true,
|
||||||
success: (data) => {
|
success: (updateJson) => {
|
||||||
const dt = new Date();
|
const dt = new Date();
|
||||||
const match = data.match(/#\s*(\d+\-\d+\-\d+):v([\d+\.\w]+)/);
|
logger.info('Update check: ' + (updateJson.version || 'unknown'));
|
||||||
logger.info('Update check: ' + (match ? match[0] : 'unknown'));
|
if (!updateJson.version) {
|
||||||
if (!match) {
|
|
||||||
const errMsg = 'No version info found';
|
const errMsg = 'No version info found';
|
||||||
UpdateModel.set({
|
UpdateModel.set({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
|
@ -114,16 +113,15 @@ const Updater = {
|
||||||
this.scheduleNextCheck();
|
this.scheduleNextCheck();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const updateMinVersionMatch = data.match(/#\s*updmin:v([\d+\.\w]+)/);
|
|
||||||
const prevLastVersion = UpdateModel.lastVersion;
|
const prevLastVersion = UpdateModel.lastVersion;
|
||||||
UpdateModel.set({
|
UpdateModel.set({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
lastCheckDate: dt,
|
lastCheckDate: dt,
|
||||||
lastSuccessCheckDate: dt,
|
lastSuccessCheckDate: dt,
|
||||||
lastVersionReleaseDate: new Date(match[1]),
|
lastVersionReleaseDate: new Date(updateJson.date),
|
||||||
lastVersion: match[2],
|
lastVersion: updateJson.version,
|
||||||
lastCheckError: null,
|
lastCheckError: null,
|
||||||
lastCheckUpdMin: updateMinVersionMatch ? updateMinVersionMatch[1] : null
|
lastCheckUpdMin: updateJson.minVersion || null
|
||||||
});
|
});
|
||||||
UpdateModel.save();
|
UpdateModel.save();
|
||||||
this.scheduleNextCheck();
|
this.scheduleNextCheck();
|
||||||
|
@ -161,7 +159,7 @@ const Updater = {
|
||||||
canAutoUpdate() {
|
canAutoUpdate() {
|
||||||
const minLauncherVersion = UpdateModel.lastCheckUpdMin;
|
const minLauncherVersion = UpdateModel.lastCheckUpdMin;
|
||||||
if (minLauncherVersion) {
|
if (minLauncherVersion) {
|
||||||
const cmp = SemVer.compareVersions(Launcher.version, minLauncherVersion);
|
const cmp = SemVer.compareVersions(RuntimeInfo.version, minLauncherVersion);
|
||||||
if (cmp < 0) {
|
if (cmp < 0) {
|
||||||
UpdateModel.set({ updateStatus: 'ready', updateManual: true });
|
UpdateModel.set({ updateStatus: 'ready', updateManual: true });
|
||||||
return false;
|
return false;
|
||||||
|
@ -182,28 +180,55 @@ const Updater = {
|
||||||
}
|
}
|
||||||
UpdateModel.set({ updateStatus: 'downloading', updateError: null });
|
UpdateModel.set({ updateStatus: 'downloading', updateError: null });
|
||||||
logger.info('Downloading update', ver);
|
logger.info('Downloading update', ver);
|
||||||
|
const updateAssetName = this.getUpdateAssetName(ver);
|
||||||
|
if (!updateAssetName) {
|
||||||
|
logger.error('Empty updater asset name for', Launcher.platform(), Launcher.arch());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const updateUrlBasePath = Links.UpdateBasePath.replace('{ver}', ver);
|
||||||
|
const updateAssetUrl = updateUrlBasePath + updateAssetName;
|
||||||
Transport.httpGet({
|
Transport.httpGet({
|
||||||
url: Links.UpdateDesktop.replace('{ver}', ver),
|
url: updateAssetUrl,
|
||||||
file: 'KeeWeb-' + ver + '.zip',
|
file: updateAssetName,
|
||||||
cache: !startedByUser,
|
cleanupOldFiles: true,
|
||||||
success: (filePath) => {
|
cache: true,
|
||||||
UpdateModel.set({ updateStatus: 'extracting' });
|
success: (assetFilePath) => {
|
||||||
logger.info('Extracting update file', this.UpdateCheckFiles, filePath);
|
logger.info('Downloading update signatures');
|
||||||
this.extractAppUpdate(filePath, (err) => {
|
Transport.httpGet({
|
||||||
if (err) {
|
url: updateUrlBasePath + 'Verify.sign.sha256',
|
||||||
logger.error('Error extracting update', err);
|
text: true,
|
||||||
|
file: updateAssetName + '.sign',
|
||||||
|
cleanupOldFiles: true,
|
||||||
|
cache: true,
|
||||||
|
success: (assetFileSignaturePath) => {
|
||||||
|
this.verifySignature(assetFilePath, updateAssetName, (err, valid) => {
|
||||||
|
if (err || !valid) {
|
||||||
|
UpdateModel.set({
|
||||||
|
updateStatus: 'error',
|
||||||
|
updateError: err
|
||||||
|
? 'Error verifying update signature'
|
||||||
|
: 'Invalid update signature'
|
||||||
|
});
|
||||||
|
Launcher.deleteFile(assetFilePath);
|
||||||
|
Launcher.deleteFile(assetFileSignaturePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info('Update is ready', assetFilePath);
|
||||||
|
UpdateModel.set({ updateStatus: 'ready', updateError: null });
|
||||||
|
if (!startedByUser) {
|
||||||
|
Events.emit('update-app');
|
||||||
|
}
|
||||||
|
if (typeof successCallback === 'function') {
|
||||||
|
successCallback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error(e) {
|
||||||
|
logger.error('Error downloading update signatures', e);
|
||||||
UpdateModel.set({
|
UpdateModel.set({
|
||||||
updateStatus: 'error',
|
updateStatus: 'error',
|
||||||
updateError: 'Error extracting update'
|
updateError: 'Error downloading update signatures'
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
UpdateModel.set({ updateStatus: 'ready', updateError: null });
|
|
||||||
if (!startedByUser) {
|
|
||||||
Events.emit('update-app');
|
|
||||||
}
|
|
||||||
if (typeof successCallback === 'function') {
|
|
||||||
successCallback();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -217,61 +242,64 @@ const Updater = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
extractAppUpdate(updateFile, cb) {
|
verifySignature(assetFilePath, assetName, callback) {
|
||||||
const expectedFiles = this.UpdateCheckFiles;
|
logger.info('Verifying update signature', assetName);
|
||||||
const appPath = Launcher.getUserDataPath();
|
const fs = Launcher.req('fs');
|
||||||
const StreamZip = Launcher.req('node-stream-zip');
|
const signaturesTxt = fs.readFileSync(assetFilePath + '.sign', 'utf8');
|
||||||
StreamZip.setFs(Launcher.req('original-fs'));
|
const assetSignatureLine = signaturesTxt
|
||||||
const zip = new StreamZip({ file: updateFile, storeEntries: true });
|
.split('\n')
|
||||||
zip.on('error', cb);
|
.find((line) => line.endsWith(assetName));
|
||||||
zip.on('ready', () => {
|
if (!assetSignatureLine) {
|
||||||
const containsAll = expectedFiles.every((expFile) => {
|
logger.error('Signature not found for asset', assetName);
|
||||||
const entry = zip.entry(expFile);
|
callback('Asset signature not found');
|
||||||
return entry && entry.isFile;
|
return;
|
||||||
|
}
|
||||||
|
const signature = kdbxweb.ByteUtils.hexToBytes(assetSignatureLine.split(' ')[0]);
|
||||||
|
const fileBytes = fs.readFileSync(assetFilePath);
|
||||||
|
SignatureVerifier.verify(fileBytes, signature)
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error('Error verifying signature', e);
|
||||||
|
callback('Error verifying signature');
|
||||||
|
})
|
||||||
|
.then((valid) => {
|
||||||
|
logger.info(`Update asset signature is ${valid ? 'valid' : 'invalid'}`);
|
||||||
|
callback(undefined, valid);
|
||||||
});
|
});
|
||||||
if (!containsAll) {
|
|
||||||
return cb('Bad archive');
|
|
||||||
}
|
|
||||||
this.validateArchiveSignature(updateFile, zip)
|
|
||||||
.then(() => {
|
|
||||||
zip.extract(null, appPath, (err) => {
|
|
||||||
zip.close();
|
|
||||||
if (err) {
|
|
||||||
return cb(err);
|
|
||||||
}
|
|
||||||
Launcher.deleteFile(updateFile);
|
|
||||||
cb();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
return cb('Invalid archive: ' + e);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
validateArchiveSignature(archivePath, zip) {
|
getUpdateAssetName(ver) {
|
||||||
if (!zip.comment) {
|
const platform = Launcher.platform();
|
||||||
return Promise.reject('No comment in ZIP');
|
const arch = Launcher.arch();
|
||||||
|
switch (platform) {
|
||||||
|
case 'win32':
|
||||||
|
switch (arch) {
|
||||||
|
case 'x64':
|
||||||
|
return `KeeWeb-${ver}.win.x64.exe`;
|
||||||
|
case 'ia32':
|
||||||
|
return `KeeWeb-${ver}.win.ia32.exe`;
|
||||||
|
case 'arm64':
|
||||||
|
return `KeeWeb-${ver}.win.arm64.exe`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'darwin':
|
||||||
|
switch (arch) {
|
||||||
|
case 'x64':
|
||||||
|
return `KeeWeb-${ver}.mac.x64.dmg`;
|
||||||
|
case 'arm64':
|
||||||
|
return `KeeWeb-${ver}.mac.arm64.dmg`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (zip.comment.length !== 512) {
|
return undefined;
|
||||||
return Promise.reject('Bad comment length in ZIP: ' + zip.comment.length);
|
},
|
||||||
}
|
|
||||||
try {
|
installAndRestart() {
|
||||||
const zipFileData = Launcher.req('fs').readFileSync(archivePath);
|
if (!Launcher) {
|
||||||
const dataToVerify = zipFileData.slice(0, zip.centralDirectory.headerOffset + 22);
|
return;
|
||||||
const signature = window.Buffer.from(zip.comment, 'hex');
|
|
||||||
return SignatureVerifier.verify(dataToVerify, signature)
|
|
||||||
.catch(() => {
|
|
||||||
throw new Error('Error verifying signature');
|
|
||||||
})
|
|
||||||
.then((isValid) => {
|
|
||||||
if (!isValid) {
|
|
||||||
throw new Error('Invalid signature');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
return Promise.reject(err.toString());
|
|
||||||
}
|
}
|
||||||
|
const updateAssetName = this.getUpdateAssetName(UpdateModel.lastVersion);
|
||||||
|
const updateFilePath = Transport.cacheFilePath(updateAssetName);
|
||||||
|
Launcher.requestRestartAndUpdate(updateFilePath);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 { Launcher } from 'comp/launcher';
|
||||||
import { Logger } from 'util/logger';
|
import { Logger } from 'util/logger';
|
||||||
import { noop } from 'util/fn';
|
import { noop } from 'util/fn';
|
||||||
|
import { StringFormat } from 'util/formatting/string-format';
|
||||||
|
|
||||||
const logger = new Logger('transport');
|
const logger = new Logger('transport');
|
||||||
|
|
||||||
const Transport = {
|
const Transport = {
|
||||||
|
cacheFilePath(fileName) {
|
||||||
|
return Launcher.getTempPath(fileName);
|
||||||
|
},
|
||||||
|
|
||||||
httpGet(config) {
|
httpGet(config) {
|
||||||
let tmpFile;
|
let tmpFile;
|
||||||
const fs = Launcher.req('fs');
|
const fs = Launcher.req('fs');
|
||||||
if (config.file) {
|
if (config.file) {
|
||||||
tmpFile = Launcher.getTempPath(config.file);
|
const baseTempPath = Launcher.getTempPath();
|
||||||
|
if (config.cleanupOldFiles) {
|
||||||
|
const allFiles = fs.readdirSync(baseTempPath);
|
||||||
|
for (const file of allFiles) {
|
||||||
|
if (
|
||||||
|
file !== config.file &&
|
||||||
|
StringFormat.replaceVersion(file, '0') ===
|
||||||
|
StringFormat.replaceVersion(config.file, '0')
|
||||||
|
) {
|
||||||
|
fs.unlinkSync(Launcher.joinPath(baseTempPath, file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tmpFile = Launcher.joinPath(baseTempPath, config.file);
|
||||||
if (fs.existsSync(tmpFile)) {
|
if (fs.existsSync(tmpFile)) {
|
||||||
try {
|
try {
|
||||||
if (config.cache && fs.statSync(tmpFile).size > 0) {
|
if (config.cache && fs.statSync(tmpFile).size > 0) {
|
||||||
|
@ -62,9 +80,16 @@ const Transport = {
|
||||||
});
|
});
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
data = window.Buffer.concat(data);
|
data = window.Buffer.concat(data);
|
||||||
if (config.utf8) {
|
if (config.text || config.json) {
|
||||||
data = data.toString('utf8');
|
data = data.toString('utf8');
|
||||||
}
|
}
|
||||||
|
if (config.json) {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
config.error('Error parsing JSON: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
config.success(data);
|
config.success(data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,9 @@ const Launcher = {
|
||||||
platform() {
|
platform() {
|
||||||
return process.platform;
|
return process.platform;
|
||||||
},
|
},
|
||||||
|
arch() {
|
||||||
|
return process.arch;
|
||||||
|
},
|
||||||
electron() {
|
electron() {
|
||||||
return this.req('electron');
|
return this.req('electron');
|
||||||
},
|
},
|
||||||
|
@ -55,7 +58,15 @@ const Launcher = {
|
||||||
return this.joinPath(this.userDataPath, fileName || '');
|
return this.joinPath(this.userDataPath, fileName || '');
|
||||||
},
|
},
|
||||||
getTempPath(fileName) {
|
getTempPath(fileName) {
|
||||||
return this.joinPath(this.remoteApp().getPath('temp'), fileName || '');
|
let tempPath = this.joinPath(this.remoteApp().getPath('temp'), 'KeeWeb');
|
||||||
|
const fs = this.req('fs');
|
||||||
|
if (!fs.existsSync(tempPath)) {
|
||||||
|
fs.mkdirSync(tempPath);
|
||||||
|
}
|
||||||
|
if (fileName) {
|
||||||
|
tempPath = this.joinPath(tempPath, fileName);
|
||||||
|
}
|
||||||
|
return tempPath;
|
||||||
},
|
},
|
||||||
getDocumentsPath(fileName) {
|
getDocumentsPath(fileName) {
|
||||||
return this.joinPath(this.remoteApp().getPath('documents'), fileName || '');
|
return this.joinPath(this.remoteApp().getPath('documents'), fileName || '');
|
||||||
|
@ -164,18 +175,18 @@ const Launcher = {
|
||||||
requestExit() {
|
requestExit() {
|
||||||
const app = this.remoteApp();
|
const app = this.remoteApp();
|
||||||
app.setHookBeforeQuitEvent(false);
|
app.setHookBeforeQuitEvent(false);
|
||||||
if (this.restartPending) {
|
if (this.pendingUpdateFile) {
|
||||||
app.restartApp();
|
app.restartAndUpdate(this.pendingUpdateFile);
|
||||||
} else {
|
} else {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
requestRestart() {
|
requestRestartAndUpdate(updateFilePath) {
|
||||||
this.restartPending = true;
|
this.pendingUpdateFile = updateFilePath;
|
||||||
this.requestExit();
|
this.requestExit();
|
||||||
},
|
},
|
||||||
cancelRestart() {
|
cancelRestart() {
|
||||||
this.restartPending = false;
|
this.pendingUpdateFile = undefined;
|
||||||
},
|
},
|
||||||
setClipboardText(text) {
|
setClipboardText(text) {
|
||||||
return this.electron().clipboard.writeText(text);
|
return this.electron().clipboard.writeText(text);
|
||||||
|
@ -203,7 +214,7 @@ const Launcher = {
|
||||||
return process.platform !== 'linux';
|
return process.platform !== 'linux';
|
||||||
},
|
},
|
||||||
updaterEnabled() {
|
updaterEnabled() {
|
||||||
return this.electron().remote.process.argv.indexOf('--disable-updater') === -1;
|
return process.platform !== 'linux';
|
||||||
},
|
},
|
||||||
getMainWindow() {
|
getMainWindow() {
|
||||||
return this.remoteApp().getMainWindow();
|
return this.remoteApp().getMainWindow();
|
||||||
|
@ -301,6 +312,18 @@ const Launcher = {
|
||||||
},
|
},
|
||||||
setGlobalShortcuts(appSettings) {
|
setGlobalShortcuts(appSettings) {
|
||||||
this.remoteApp().setGlobalShortcuts(appSettings);
|
this.remoteApp().setGlobalShortcuts(appSettings);
|
||||||
|
},
|
||||||
|
minimizeMainWindow() {
|
||||||
|
this.getMainWindow().minimize();
|
||||||
|
},
|
||||||
|
maximizeMainWindow() {
|
||||||
|
this.getMainWindow().maximize();
|
||||||
|
},
|
||||||
|
restoreMainWindow() {
|
||||||
|
this.getMainWindow().restore();
|
||||||
|
},
|
||||||
|
mainWindowMaximized() {
|
||||||
|
return this.getMainWindow().isMaximized();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -308,6 +331,8 @@ Events.on('launcher-exit-request', () => {
|
||||||
setTimeout(() => Launcher.exit(), 0);
|
setTimeout(() => Launcher.exit(), 0);
|
||||||
});
|
});
|
||||||
Events.on('launcher-minimize', () => setTimeout(() => Events.emit('app-minimized'), 0));
|
Events.on('launcher-minimize', () => setTimeout(() => Events.emit('app-minimized'), 0));
|
||||||
|
Events.on('launcher-maximize', () => setTimeout(() => Events.emit('app-maximized'), 0));
|
||||||
|
Events.on('launcher-unmaximize', () => setTimeout(() => Events.emit('app-unmaximized'), 0));
|
||||||
Events.on('launcher-started-minimized', () => setTimeout(() => Launcher.minimizeApp(), 0));
|
Events.on('launcher-started-minimized', () => setTimeout(() => Launcher.minimizeApp(), 0));
|
||||||
Events.on('start-profile', (data) => StartProfiler.reportAppProfile(data));
|
Events.on('start-profile', (data) => StartProfiler.reportAppProfile(data));
|
||||||
Events.on('log', (e) => new Logger(e.category || 'remote-app')[e.method || 'info'](e.message));
|
Events.on('log', (e) => new Logger(e.category || 'remote-app')[e.method || 'info'](e.message));
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import kdbxweb from 'kdbxweb';
|
||||||
import { Events } from 'framework/events';
|
import { Events } from 'framework/events';
|
||||||
import { Logger } from 'util/logger';
|
import { Logger } from 'util/logger';
|
||||||
import { Launcher } from 'comp/launcher';
|
import { Launcher } from 'comp/launcher';
|
||||||
|
@ -8,11 +9,18 @@ let NativeModules;
|
||||||
if (Launcher) {
|
if (Launcher) {
|
||||||
const logger = new Logger('native-module-connector');
|
const logger = new Logger('native-module-connector');
|
||||||
|
|
||||||
let host;
|
let hostRunning = false;
|
||||||
|
let hostStartPromise;
|
||||||
let callId = 0;
|
let callId = 0;
|
||||||
let promises = {};
|
let promises = {};
|
||||||
let ykChalRespCallbacks = {};
|
let ykChalRespCallbacks = {};
|
||||||
|
|
||||||
|
const { ipcRenderer } = Launcher.electron();
|
||||||
|
ipcRenderer.on('nativeModuleCallback', (e, msg) => NativeModules.hostCallback(msg));
|
||||||
|
ipcRenderer.on('nativeModuleHostError', (e, err) => NativeModules.hostError(err));
|
||||||
|
ipcRenderer.on('nativeModuleHostExit', (e, { code, sig }) => NativeModules.hostExit(code, sig));
|
||||||
|
ipcRenderer.on('nativeModuleHostDisconnect', () => NativeModules.hostDisconnect());
|
||||||
|
|
||||||
const handlers = {
|
const handlers = {
|
||||||
yubikeys(numYubiKeys) {
|
yubikeys(numYubiKeys) {
|
||||||
Events.emit('native-modules-yubikeys', { numYubiKeys });
|
Events.emit('native-modules-yubikeys', { numYubiKeys });
|
||||||
|
@ -35,7 +43,7 @@ if (Launcher) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'yk-chal-resp-result'({ callbackId, error, result }) {
|
yubiKeyChallengeResponseResult({ callbackId, error, result }) {
|
||||||
const callback = ykChalRespCallbacks[callbackId];
|
const callback = ykChalRespCallbacks[callbackId];
|
||||||
if (callback) {
|
if (callback) {
|
||||||
const willBeCalledAgain = error && error.touchRequested;
|
const willBeCalledAgain = error && error.touchRequested;
|
||||||
|
@ -49,39 +57,39 @@ if (Launcher) {
|
||||||
|
|
||||||
NativeModules = {
|
NativeModules = {
|
||||||
startHost() {
|
startHost() {
|
||||||
if (host) {
|
if (hostRunning) {
|
||||||
return;
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (hostStartPromise) {
|
||||||
|
return hostStartPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('Starting native module host');
|
logger.debug('Starting native module host');
|
||||||
|
|
||||||
const path = Launcher.req('path');
|
hostStartPromise = this.callNoWait('start').then(() => {
|
||||||
const appContentRoot = Launcher.remoteApp().getAppContentRoot();
|
hostStartPromise = undefined;
|
||||||
const mainModulePath = path.join(appContentRoot, 'native-module-host.js');
|
hostRunning = true;
|
||||||
|
|
||||||
const { fork } = Launcher.req('child_process');
|
if (this.usbListenerRunning) {
|
||||||
|
return this.call('startUsbListener');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
host = fork(mainModulePath);
|
return hostStartPromise;
|
||||||
|
|
||||||
host.on('message', (message) => this.hostCallback(message));
|
|
||||||
|
|
||||||
host.on('error', (e) => this.hostError(e));
|
|
||||||
host.on('exit', (code, sig) => this.hostExit(code, sig));
|
|
||||||
|
|
||||||
this.call('init', Launcher.remoteApp().getAppMainRoot());
|
|
||||||
|
|
||||||
if (this.usbListenerRunning) {
|
|
||||||
this.call('start-usb');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
hostError(e) {
|
hostError(e) {
|
||||||
logger.error('Host error', e);
|
logger.error('Host error', e);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hostDisconnect() {
|
||||||
|
logger.error('Host disconnected');
|
||||||
|
},
|
||||||
|
|
||||||
hostExit(code, sig) {
|
hostExit(code, sig) {
|
||||||
logger.error(`Host exited with code ${code} and signal ${sig}`);
|
logger.error(`Host exited with code ${code} and signal ${sig}`);
|
||||||
host = null;
|
|
||||||
|
hostRunning = false;
|
||||||
|
|
||||||
const err = new Error('Native module host crashed');
|
const err = new Error('Native module host crashed');
|
||||||
|
|
||||||
|
@ -121,56 +129,109 @@ if (Launcher) {
|
||||||
},
|
},
|
||||||
|
|
||||||
call(cmd, ...args) {
|
call(cmd, ...args) {
|
||||||
return new Promise((resolve, reject) => {
|
return this.startHost().then(() => this.callNoWait(cmd, ...args));
|
||||||
if (!host) {
|
},
|
||||||
try {
|
|
||||||
this.startHost();
|
|
||||||
} catch (e) {
|
|
||||||
return reject(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
callNoWait(cmd, ...args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
callId++;
|
callId++;
|
||||||
if (callId === Number.MAX_SAFE_INTEGER) {
|
if (callId === Number.MAX_SAFE_INTEGER) {
|
||||||
callId = 1;
|
callId = 1;
|
||||||
}
|
}
|
||||||
// logger.debug('Call', cmd, args, callId);
|
// logger.debug('Call', cmd, args, callId);
|
||||||
promises[callId] = { cmd, resolve, reject };
|
promises[callId] = { cmd, resolve, reject };
|
||||||
host.send({ cmd, args, callId });
|
|
||||||
|
ipcRenderer.send('nativeModuleCall', { cmd, args, callId });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
startUsbListener() {
|
startUsbListener() {
|
||||||
this.call('start-usb');
|
this.call('startUsbListener');
|
||||||
this.usbListenerRunning = true;
|
this.usbListenerRunning = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
stopUsbListener() {
|
stopUsbListener() {
|
||||||
this.usbListenerRunning = false;
|
this.usbListenerRunning = false;
|
||||||
if (host) {
|
if (hostRunning) {
|
||||||
this.call('stop-usb');
|
this.call('stopUsbListener');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getYubiKeys(config) {
|
getYubiKeys(config) {
|
||||||
return this.call('get-yubikeys', config);
|
return this.call('getYubiKeys', config);
|
||||||
},
|
},
|
||||||
|
|
||||||
yubiKeyChallengeResponse(yubiKey, challenge, slot, callback) {
|
yubiKeyChallengeResponse(yubiKey, challenge, slot, callback) {
|
||||||
ykChalRespCallbacks[callId] = callback;
|
ykChalRespCallbacks[callId] = callback;
|
||||||
return this.call('yk-chal-resp', yubiKey, challenge, slot, callId);
|
return this.call('yubiKeyChallengeResponse', yubiKey, challenge, slot, callId);
|
||||||
},
|
},
|
||||||
|
|
||||||
yubiKeyCancelChallengeResponse() {
|
yubiKeyCancelChallengeResponse() {
|
||||||
if (host) {
|
if (hostRunning) {
|
||||||
this.call('yk-cancel-chal-resp');
|
this.call('yubiKeyCancelChallengeResponse');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
argon2(password, salt, options) {
|
argon2(password, salt, options) {
|
||||||
return this.call('argon2', password, salt, options);
|
return this.call('argon2', password, salt, options);
|
||||||
|
},
|
||||||
|
|
||||||
|
hardwareEncrypt: async (value) => {
|
||||||
|
const { data, salt } = await ipcRenderer.invoke('hardwareEncrypt', value.dataAndSalt());
|
||||||
|
return new kdbxweb.ProtectedValue(data, salt);
|
||||||
|
},
|
||||||
|
|
||||||
|
hardwareDecrypt: async (value, touchIdPrompt) => {
|
||||||
|
const { data, salt } = await ipcRenderer.invoke(
|
||||||
|
'hardwareDecrypt',
|
||||||
|
value.dataAndSalt(),
|
||||||
|
touchIdPrompt
|
||||||
|
);
|
||||||
|
return new kdbxweb.ProtectedValue(data, salt);
|
||||||
|
},
|
||||||
|
|
||||||
|
kbdGetActiveWindow(options) {
|
||||||
|
return this.call('kbdGetActiveWindow', options);
|
||||||
|
},
|
||||||
|
|
||||||
|
kbdGetActivePid() {
|
||||||
|
return this.call('kbdGetActivePid');
|
||||||
|
},
|
||||||
|
|
||||||
|
kbdShowWindow(win) {
|
||||||
|
return this.call('kbdShowWindow', win);
|
||||||
|
},
|
||||||
|
|
||||||
|
kbdText(str) {
|
||||||
|
return this.call('kbdText', str);
|
||||||
|
},
|
||||||
|
|
||||||
|
kbdTextAsKeys(str, mods) {
|
||||||
|
return this.call('kbdTextAsKeys', str, mods);
|
||||||
|
},
|
||||||
|
|
||||||
|
kbdKeyPress(code, modifiers) {
|
||||||
|
return this.call('kbdKeyPress', code, modifiers);
|
||||||
|
},
|
||||||
|
|
||||||
|
kbdShortcut(code, modifiers) {
|
||||||
|
return this.call('kbdShortcut', code, modifiers);
|
||||||
|
},
|
||||||
|
|
||||||
|
kbdKeyMoveWithModifier(down, modifiers) {
|
||||||
|
return this.call('kbdKeyMoveWithModifier', down, modifiers);
|
||||||
|
},
|
||||||
|
|
||||||
|
kbdKeyPressWithCharacter(character, code, modifiers) {
|
||||||
|
return this.call('kbdKeyPressWithCharacter', character, code, modifiers);
|
||||||
|
},
|
||||||
|
|
||||||
|
kbdEnsureModifierNotPressed() {
|
||||||
|
return this.call('kbdEnsureModifierNotPressed');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
global.NativeModules = NativeModules;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { NativeModules };
|
export { NativeModules };
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { Events } from 'framework/events';
|
import { Events } from 'framework/events';
|
||||||
import { Features } from 'util/features';
|
import { Features } from 'util/features';
|
||||||
import { Locale } from 'util/locale';
|
import { Locale } from 'util/locale';
|
||||||
|
import { ThemeWatcher } from 'comp/browser/theme-watcher';
|
||||||
|
import { AppSettingsModel } from 'models/app-settings-model';
|
||||||
|
import { Logger } from 'util/logger';
|
||||||
|
|
||||||
|
const logger = new Logger('settings-manager');
|
||||||
|
|
||||||
const SettingsManager = {
|
const SettingsManager = {
|
||||||
neutralLocale: null,
|
neutralLocale: null,
|
||||||
|
@ -16,23 +21,65 @@ const SettingsManager = {
|
||||||
allThemes: {
|
allThemes: {
|
||||||
dark: 'setGenThemeDark',
|
dark: 'setGenThemeDark',
|
||||||
light: 'setGenThemeLight',
|
light: 'setGenThemeLight',
|
||||||
fb: 'setGenThemeFb',
|
|
||||||
db: 'setGenThemeDb',
|
|
||||||
sd: 'setGenThemeSd',
|
sd: 'setGenThemeSd',
|
||||||
sl: 'setGenThemeSl',
|
sl: 'setGenThemeSl',
|
||||||
|
fb: 'setGenThemeFb',
|
||||||
|
bl: 'setGenThemeBl',
|
||||||
|
db: 'setGenThemeDb',
|
||||||
|
lb: 'setGenThemeLb',
|
||||||
te: 'setGenThemeTe',
|
te: 'setGenThemeTe',
|
||||||
|
lt: 'setGenThemeLt',
|
||||||
|
dc: 'setGenThemeDc',
|
||||||
hc: 'setGenThemeHc'
|
hc: 'setGenThemeHc'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// changing something here? don't forget about desktop/app.js
|
||||||
|
autoSwitchedThemes: [
|
||||||
|
{
|
||||||
|
name: 'setGenThemeDefault',
|
||||||
|
dark: 'dark',
|
||||||
|
light: 'light'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'setGenThemeSol',
|
||||||
|
dark: 'sd',
|
||||||
|
light: 'sl'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'setGenThemeBlue',
|
||||||
|
dark: 'fb',
|
||||||
|
light: 'bl'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'setGenThemeBrown',
|
||||||
|
dark: 'db',
|
||||||
|
light: 'lb'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'setGenThemeTerminal',
|
||||||
|
dark: 'te',
|
||||||
|
light: 'lt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'setGenThemeHighContrast',
|
||||||
|
dark: 'dc',
|
||||||
|
light: 'hc'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
customLocales: {},
|
customLocales: {},
|
||||||
|
|
||||||
setBySettings(settings) {
|
init() {
|
||||||
this.setTheme(settings.theme);
|
Events.on('dark-mode-changed', () => this.darkModeChanged());
|
||||||
this.setFontSize(settings.fontSize);
|
},
|
||||||
const locale = settings.locale;
|
|
||||||
|
setBySettings() {
|
||||||
|
this.setTheme(AppSettingsModel.theme);
|
||||||
|
this.setFontSize(AppSettingsModel.fontSize);
|
||||||
|
const locale = AppSettingsModel.locale;
|
||||||
try {
|
try {
|
||||||
if (locale) {
|
if (locale) {
|
||||||
this.setLocale(settings.locale);
|
this.setLocale(AppSettingsModel.locale);
|
||||||
} else {
|
} else {
|
||||||
this.setLocale(this.getBrowserLocale());
|
this.setLocale(this.getBrowserLocale());
|
||||||
}
|
}
|
||||||
|
@ -55,18 +102,45 @@ const SettingsManager = {
|
||||||
document.body.classList.remove(cls);
|
document.body.classList.remove(cls);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (AppSettingsModel.autoSwitchTheme) {
|
||||||
|
theme = this.selectDarkOrLightTheme(theme);
|
||||||
|
}
|
||||||
document.body.classList.add(this.getThemeClass(theme));
|
document.body.classList.add(this.getThemeClass(theme));
|
||||||
const metaThemeColor = document.head.querySelector('meta[name=theme-color]');
|
const metaThemeColor = document.head.querySelector('meta[name=theme-color]');
|
||||||
if (metaThemeColor) {
|
if (metaThemeColor) {
|
||||||
metaThemeColor.content = window.getComputedStyle(document.body).backgroundColor;
|
metaThemeColor.content = window.getComputedStyle(document.body).backgroundColor;
|
||||||
}
|
}
|
||||||
this.activeTheme = theme;
|
this.activeTheme = theme;
|
||||||
|
logger.debug('Theme changed', theme);
|
||||||
|
Events.emit('theme-applied');
|
||||||
},
|
},
|
||||||
|
|
||||||
getThemeClass(theme) {
|
getThemeClass(theme) {
|
||||||
return 'th-' + theme;
|
return 'th-' + theme;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
selectDarkOrLightTheme(theme) {
|
||||||
|
for (const config of this.autoSwitchedThemes) {
|
||||||
|
if (config.light === theme || config.dark === theme) {
|
||||||
|
return ThemeWatcher.dark ? config.dark : config.light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return theme;
|
||||||
|
},
|
||||||
|
|
||||||
|
darkModeChanged() {
|
||||||
|
if (AppSettingsModel.autoSwitchTheme) {
|
||||||
|
for (const config of this.autoSwitchedThemes) {
|
||||||
|
if (config.light === this.activeTheme || config.dark === this.activeTheme) {
|
||||||
|
const newTheme = ThemeWatcher.dark ? config.dark : config.light;
|
||||||
|
logger.debug('Setting theme triggered by system settings change', newTheme);
|
||||||
|
this.setTheme(newTheme);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setFontSize(fontSize) {
|
setFontSize(fontSize) {
|
||||||
const defaultFontSize = Features.isMobile ? 14 : 12;
|
const defaultFontSize = Features.isMobile ? 14 : 12;
|
||||||
document.documentElement.style.fontSize = defaultFontSize + (fontSize || 0) * 2 + 'px';
|
document.documentElement.style.fontSize = defaultFontSize + (fontSize || 0) * 2 + 'px';
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const DefaultAppSettings = {
|
const DefaultAppSettings = {
|
||||||
theme: null, // UI theme
|
theme: null, // UI theme
|
||||||
|
autoSwitchTheme: false, // automatically switch between light and dark theme
|
||||||
locale: null, // user interface language
|
locale: null, // user interface language
|
||||||
expandGroups: true, // show entries from all subgroups
|
expandGroups: true, // show entries from all subgroups
|
||||||
listViewWidth: null, // width of the entry list representation
|
listViewWidth: null, // width of the entry list representation
|
||||||
|
@ -12,6 +13,7 @@ const DefaultAppSettings = {
|
||||||
rememberKeyFiles: 'path', // remember keyfiles selected on the Open screen
|
rememberKeyFiles: 'path', // remember keyfiles selected on the Open screen
|
||||||
idleMinutes: 15, // app lock timeout after inactivity, minutes
|
idleMinutes: 15, // app lock timeout after inactivity, minutes
|
||||||
minimizeOnClose: false, // minimise the app instead of closing
|
minimizeOnClose: false, // minimise the app instead of closing
|
||||||
|
minimizeOnFieldCopy: false, // minimise the app on copy
|
||||||
tableView: false, // view entries as a table instead of list
|
tableView: false, // view entries as a table instead of list
|
||||||
colorfulIcons: false, // use colorful custom icons instead of grayscale
|
colorfulIcons: false, // use colorful custom icons instead of grayscale
|
||||||
useMarkdown: true, // use Markdown in Notes field
|
useMarkdown: true, // use Markdown in Notes field
|
||||||
|
@ -36,6 +38,15 @@ const DefaultAppSettings = {
|
||||||
useGroupIconForEntries: false, // automatically use group icon when creating new entries
|
useGroupIconForEntries: false, // automatically use group icon when creating new entries
|
||||||
enableUsb: true, // enable interaction with USB devices
|
enableUsb: true, // enable interaction with USB devices
|
||||||
fieldLabelDblClickAutoType: false, // trigger auto-type by doubleclicking field label
|
fieldLabelDblClickAutoType: false, // trigger auto-type by doubleclicking field label
|
||||||
|
auditPasswords: true, // enable password audit
|
||||||
|
auditPasswordEntropy: true, // show warnings for weak passwords
|
||||||
|
excludePinsFromAudit: true, // exclude PIN codes from audit
|
||||||
|
checkPasswordsOnHIBP: false, // check passwords on Have I Been Pwned
|
||||||
|
auditPasswordAge: 0, // show warnings about old passwords, number of years, 0 = disabled
|
||||||
|
useLegacyAutoType: false, // use legacy auto-type engine (will be removed in future versions)
|
||||||
|
deviceOwnerAuth: null, // Touch ID: null / 'memory' / 'file'
|
||||||
|
deviceOwnerAuthTimeoutMinutes: 0, // how often master password is required with Touch ID
|
||||||
|
disableOfflineStorage: false, // don't cache loaded files in offline storage
|
||||||
|
|
||||||
yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey
|
yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey
|
||||||
yubiKeyAutoOpen: false, // auto-load one-time codes when there are open files
|
yubiKeyAutoOpen: false, // auto-load one-time codes when there are open files
|
||||||
|
|
|
@ -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',
|
License: 'https://github.com/keeweb/keeweb/blob/master/LICENSE',
|
||||||
LicenseApache: 'https://opensource.org/licenses/Apache-2.0',
|
LicenseApache: 'https://opensource.org/licenses/Apache-2.0',
|
||||||
LicenseLinkCCBY40: 'https://creativecommons.org/licenses/by/4.0/',
|
LicenseLinkCCBY40: 'https://creativecommons.org/licenses/by/4.0/',
|
||||||
UpdateDesktop: 'https://github.com/keeweb/keeweb/releases/download/v{ver}/UpdateDesktop.zip',
|
UpdateBasePath: 'https://github.com/keeweb/keeweb/releases/download/v{ver}/',
|
||||||
ReleaseNotes: 'https://github.com/keeweb/keeweb/blob/master/release-notes.md#release-notes',
|
ReleaseNotes: 'https://github.com/keeweb/keeweb/blob/master/release-notes.md#release-notes',
|
||||||
SelfHostedDropbox: 'https://github.com/keeweb/keeweb#self-hosting',
|
SelfHostedDropbox: 'https://github.com/keeweb/keeweb#self-hosting',
|
||||||
Manifest: 'https://app.keeweb.info/manifest.appcache',
|
UpdateJson: 'https://app.keeweb.info/update.json',
|
||||||
AutoType: 'https://github.com/keeweb/keeweb/wiki/Auto-Type',
|
AutoType: 'https://github.com/keeweb/keeweb/wiki/Auto-Type',
|
||||||
|
AutoTypeMacOS: 'https://github.com/keeweb/keeweb/wiki/Auto-Type#macos',
|
||||||
Translation: 'https://keeweb.oneskyapp.com/',
|
Translation: 'https://keeweb.oneskyapp.com/',
|
||||||
Donation: 'https://opencollective.com/keeweb#support',
|
Donation: 'https://opencollective.com/keeweb#support',
|
||||||
Plugins: 'https://plugins.keeweb.info',
|
Plugins: 'https://plugins.keeweb.info',
|
||||||
PluginDevelopStart: 'https://github.com/keeweb/keeweb/wiki/Plugins',
|
PluginDevelopStart: 'https://github.com/keeweb/keeweb/wiki/Plugins',
|
||||||
YubiKeyManual: 'https://github.com/keeweb/keeweb/wiki/YubiKey',
|
YubiKeyManual: 'https://github.com/keeweb/keeweb/wiki/YubiKey',
|
||||||
YubiKeyManagerInstall: 'https://github.com/Yubico/yubikey-manager#installation'
|
YubiKeyManagerInstall: 'https://github.com/Yubico/yubikey-manager#installation',
|
||||||
|
HaveIBeenPwned: 'https://haveibeenpwned.com',
|
||||||
|
HaveIBeenPwnedPrivacy: 'https://haveibeenpwned.com/Passwords'
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Links };
|
export { Links };
|
||||||
|
|
|
@ -16,7 +16,9 @@ const Timeouts = {
|
||||||
ExternalDeviceReconnect: 3000,
|
ExternalDeviceReconnect: 3000,
|
||||||
ExternalDeviceAfterReconnect: 1000,
|
ExternalDeviceAfterReconnect: 1000,
|
||||||
FieldLabelDoubleClick: 300,
|
FieldLabelDoubleClick: 300,
|
||||||
NativeModuleHostRestartTime: 3000
|
NativeModuleHostRestartTime: 3000,
|
||||||
|
FastAnimation: 100,
|
||||||
|
AutoTypeCopyPaste: 300
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Timeouts };
|
export { Timeouts };
|
||||||
|
|
|
@ -12,10 +12,14 @@ Handlebars.registerHelper('res', function (key, options) {
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
|
|
||||||
Handlebars.registerHelper('Res', (key) => {
|
Handlebars.registerHelper('Res', (key, options) => {
|
||||||
let value = Locale[key];
|
let value = Locale[key];
|
||||||
if (value) {
|
if (value) {
|
||||||
value = value[0].toUpperCase() + value.substr(1);
|
value = value[0].toUpperCase() + value.substr(1);
|
||||||
|
const ix = value.indexOf('{}');
|
||||||
|
if (ix >= 0) {
|
||||||
|
value = value.replace('{}', options.fn(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,6 +26,15 @@
|
||||||
"shiftKey": "shift",
|
"shiftKey": "shift",
|
||||||
"altKey": "alt",
|
"altKey": "alt",
|
||||||
"error": "error",
|
"error": "error",
|
||||||
|
"oneMinute": "one minute",
|
||||||
|
"minutes": "{} minutes",
|
||||||
|
"oneHour": "one hour",
|
||||||
|
"hours": "{} hours",
|
||||||
|
"oneDay": "one day",
|
||||||
|
"days": "{} days",
|
||||||
|
"oneWeek": "one week",
|
||||||
|
"oneMonth": "one month",
|
||||||
|
"oneYear": "one year",
|
||||||
|
|
||||||
"cache": "cache",
|
"cache": "cache",
|
||||||
"file": "file",
|
"file": "file",
|
||||||
|
@ -301,6 +310,16 @@
|
||||||
"detRevealField": "Reveal",
|
"detRevealField": "Reveal",
|
||||||
"detHideField": "Hide",
|
"detHideField": "Hide",
|
||||||
"detAutoTypeField": "Auto type",
|
"detAutoTypeField": "Auto type",
|
||||||
|
"detIssuesHideTooltip": "Hide this warning",
|
||||||
|
"detIssueWeakPassword": "The password is weak, it's recommended to change it.",
|
||||||
|
"detIssuePoorPassword": "The password is very weak, it's strongly recommended to change it.",
|
||||||
|
"detIssuePwnedPassword": "This password has been exposed in a data breach according to {}, it's recommended to change it.",
|
||||||
|
"detIssuePasswordCheckError": "There was an error checking password strength online.",
|
||||||
|
"detIssueOldPassword": "The password is old.",
|
||||||
|
"detIssueCloseAlertHeader": "Hide password issues",
|
||||||
|
"detIssueCloseAlertBody": "There are different ways you can hide this warning:",
|
||||||
|
"detIssueCloseAlertEntry": "Don't show for this entry",
|
||||||
|
"detIssueCloseAlertSettings": "Adjust global settings",
|
||||||
|
|
||||||
"autoTypeEntryFields": "Entry fields",
|
"autoTypeEntryFields": "Entry fields",
|
||||||
"autoTypeModifiers": "Modifier keys",
|
"autoTypeModifiers": "Modifier keys",
|
||||||
|
@ -308,6 +327,7 @@
|
||||||
"autoTypeLink": "more...",
|
"autoTypeLink": "more...",
|
||||||
"autoTypeError": "Auto-type error",
|
"autoTypeError": "Auto-type error",
|
||||||
"autoTypeErrorGeneric": "There was an error performing auto-type: {}",
|
"autoTypeErrorGeneric": "There was an error performing auto-type: {}",
|
||||||
|
"autoTypeErrorAccessibilityMacOS": "We tried to send keystrokes to the application, but it doesn't seem to work. This may happen because of missing permissions, click here to read more about it:",
|
||||||
"autoTypeErrorGlobal": "To use a system-wide shortcut, please focus the app where you want to type your password",
|
"autoTypeErrorGlobal": "To use a system-wide shortcut, please focus the app where you want to type your password",
|
||||||
"autoTypeErrorNotInstalled": "{} is not installed",
|
"autoTypeErrorNotInstalled": "{} is not installed",
|
||||||
"autoTypeHeader": "Auto-Type: Select",
|
"autoTypeHeader": "Auto-Type: Select",
|
||||||
|
@ -367,19 +387,30 @@
|
||||||
"setGenExtractingUpdate": "Extracting update...",
|
"setGenExtractingUpdate": "Extracting update...",
|
||||||
"setGenCheckErr": "There was an error downloading new version",
|
"setGenCheckErr": "There was an error downloading new version",
|
||||||
"setGenNeverChecked": "Never checked for updates",
|
"setGenNeverChecked": "Never checked for updates",
|
||||||
"setGenRestartToUpdate": "Restart the app to update",
|
"setGenRestartToUpdate": "Restart KeeWeb to update",
|
||||||
"setGenDownloadAndRestart": "Download update and restart",
|
"setGenDownloadAndRestart": "Download update and restart",
|
||||||
"setGenAppearance": "Appearance",
|
"setGenAppearance": "Appearance",
|
||||||
"setGenTheme": "Theme",
|
"setGenTheme": "Theme",
|
||||||
|
"setGenThemeDefault": "Default",
|
||||||
"setGenThemeDark": "Dark",
|
"setGenThemeDark": "Dark",
|
||||||
"setGenThemeLight": "Light",
|
"setGenThemeLight": "Light",
|
||||||
"setGenThemeFb": "Flat blue",
|
"setGenThemeBlue": "Flat blue",
|
||||||
|
"setGenThemeFb": "Dark blue",
|
||||||
|
"setGenThemeBl": "Light blue",
|
||||||
|
"setGenThemeBrown": "Brownie",
|
||||||
"setGenThemeDb": "Dark brown",
|
"setGenThemeDb": "Dark brown",
|
||||||
|
"setGenThemeLb": "Light brown",
|
||||||
|
"setGenThemeTerminal": "Terminal",
|
||||||
"setGenThemeTe": "Terminal",
|
"setGenThemeTe": "Terminal",
|
||||||
|
"setGenThemeLt": "Terminal light",
|
||||||
|
"setGenThemeHighContrast": "High contrast",
|
||||||
"setGenThemeHc": "High contrast",
|
"setGenThemeHc": "High contrast",
|
||||||
|
"setGenThemeDc": "Dark contrast",
|
||||||
|
"setGenThemeSol": "Solarized",
|
||||||
"setGenThemeSd": "Solarized dark",
|
"setGenThemeSd": "Solarized dark",
|
||||||
"setGenThemeSl": "Solarized light",
|
"setGenThemeSl": "Solarized light",
|
||||||
"setGenMoreThemes": "More themes",
|
"setGenMoreThemes": "More themes",
|
||||||
|
"setGenAutoSwitchTheme": "Automatically switch between light and dark theme when possible",
|
||||||
"setGenLocale": "Language",
|
"setGenLocale": "Language",
|
||||||
"setGenLocOther": "other languages are available as plugins",
|
"setGenLocOther": "other languages are available as plugins",
|
||||||
"setGenFontSize": "Font size",
|
"setGenFontSize": "Font size",
|
||||||
|
@ -416,12 +447,14 @@
|
||||||
"setGenClearSeconds": "In {} seconds",
|
"setGenClearSeconds": "In {} seconds",
|
||||||
"setGenClearMinute": "In a minute",
|
"setGenClearMinute": "In a minute",
|
||||||
"setGenMinInstead": "Minimize the app instead of close",
|
"setGenMinInstead": "Minimize the app instead of close",
|
||||||
|
"setGenMinOnFieldCopy": "Minimize on field copy",
|
||||||
"setGenLock": "Auto lock",
|
"setGenLock": "Auto lock",
|
||||||
"setGenLockMinimize": "When the app is minimized",
|
"setGenLockMinimize": "When the app is minimized",
|
||||||
"setGenLockCopy": "On password copy",
|
"setGenLockCopy": "On password copy",
|
||||||
"setGenLockAutoType": "On auto-type",
|
"setGenLockAutoType": "On auto-type",
|
||||||
"setGenLockOrSleep": "When the computer is locked or put to sleep",
|
"setGenLockOrSleep": "When the computer is locked or put to sleep",
|
||||||
"setGenStorage": "Storage",
|
"setGenStorage": "Storage",
|
||||||
|
"setGenDisableOfflineStorage": "Don't cache loaded files in offline storage",
|
||||||
"setGenStorageLogout": "Log out",
|
"setGenStorageLogout": "Log out",
|
||||||
"setGenShowAdvanced": "Show advanced settings",
|
"setGenShowAdvanced": "Show advanced settings",
|
||||||
"setGenDevTools": "Show dev tools",
|
"setGenDevTools": "Show dev tools",
|
||||||
|
@ -431,6 +464,23 @@
|
||||||
"setGenShowAppLogs": "Show app logs",
|
"setGenShowAppLogs": "Show app logs",
|
||||||
"setGenReloadApp": "Reload the app",
|
"setGenReloadApp": "Reload the app",
|
||||||
"setGenFieldLabelDblClickAutoType": "Auto-type on double-clicking field labels",
|
"setGenFieldLabelDblClickAutoType": "Auto-type on double-clicking field labels",
|
||||||
|
"setGenUseLegacyAutoType": "Use legacy auto-type (if you have issues)",
|
||||||
|
"setGenTouchId": "Touch ID",
|
||||||
|
"setGenTouchIdDisabled": "Don't use Touch ID",
|
||||||
|
"setGenTouchIdMemory": "Unlock with Touch ID only while KeeWeb is running",
|
||||||
|
"setGenTouchIdFile": "Always use Touch ID instead of master password",
|
||||||
|
"setGenTouchIdPass": "Require master password after",
|
||||||
|
"setGenAudit": "Audit",
|
||||||
|
"setGenAuditPasswords": "Show warnings about password strength",
|
||||||
|
"setGenAuditPasswordEntropy": "Check password length and randomness",
|
||||||
|
"setGenExcludePinsFromAudit": "Never check short numeric PIN codes, such as 123456",
|
||||||
|
"setGenCheckPasswordsOnHIBP": "Check passwords using an online service {}",
|
||||||
|
"setGenHelpHIBP": "KeeWeb can check if your passwords have been previously exposed in a data breach using an online service. Your password cannot be recovered based on data sent online, however the number of passwords checked this way may be exposed. More about your privacy when using this service can be found {}. If this option is enabled, KeeWeb will automatically check your passwords there.",
|
||||||
|
"setGenHelpHIBPLink": "here",
|
||||||
|
"setGenAuditPasswordAge": "Old passwords",
|
||||||
|
"setGenAuditPasswordAgeOff": "Don't show warnings about old passwords",
|
||||||
|
"setGenAuditPasswordAgeOneYear": "Show warnings for passwords older than one year",
|
||||||
|
"setGenAuditPasswordAgeYears": "Show warnings for passwords older than {} years",
|
||||||
|
|
||||||
"setFilePath": "File path",
|
"setFilePath": "File path",
|
||||||
"setFileStorage": "This file is loaded from {}.",
|
"setFileStorage": "This file is loaded from {}.",
|
||||||
|
@ -661,5 +711,7 @@
|
||||||
"yubiKeyTouchRequestedBody": "Please touch your YubiKey with serial number {}",
|
"yubiKeyTouchRequestedBody": "Please touch your YubiKey with serial number {}",
|
||||||
"yubiKeyDisabledErrorHeader": "USB is disabled",
|
"yubiKeyDisabledErrorHeader": "USB is disabled",
|
||||||
"yubiKeyDisabledErrorBody": "YubiKey is required to open this file, please enable USB devices in settings.",
|
"yubiKeyDisabledErrorBody": "YubiKey is required to open this file, please enable USB devices in settings.",
|
||||||
"yubiKeyErrorWithCode": "YubiKey error, code {}."
|
"yubiKeyErrorWithCode": "YubiKey error, code {}.",
|
||||||
|
|
||||||
|
"bioOpenAuthPrompt": "open \"{}\""
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,15 @@
|
||||||
"shiftKey": "Umschalt",
|
"shiftKey": "Umschalt",
|
||||||
"altKey": "Alt",
|
"altKey": "Alt",
|
||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
|
"oneMinute": "Eine Minute",
|
||||||
|
"minutes": "{} Minuten",
|
||||||
|
"oneHour": "Eine Stunde",
|
||||||
|
"hours": "{} Stunden",
|
||||||
|
"oneDay": "Ein Tag",
|
||||||
|
"days": "{} Tage",
|
||||||
|
"oneWeek": "Eine Woche",
|
||||||
|
"oneMonth": "Ein Monat",
|
||||||
|
"oneYear": "Ein Jahr",
|
||||||
"cache": "Cache",
|
"cache": "Cache",
|
||||||
"file": "Datei",
|
"file": "Datei",
|
||||||
"device": "Gerät",
|
"device": "Gerät",
|
||||||
|
@ -408,6 +417,11 @@
|
||||||
"setGenShowAppLogs": "App-Logs anzeigen",
|
"setGenShowAppLogs": "App-Logs anzeigen",
|
||||||
"setGenReloadApp": "App neu laden",
|
"setGenReloadApp": "App neu laden",
|
||||||
"setGenFieldLabelDblClickAutoType": "Auto-Type durch Anklicken von Beschriftungen aktivieren",
|
"setGenFieldLabelDblClickAutoType": "Auto-Type durch Anklicken von Beschriftungen aktivieren",
|
||||||
|
"setGenTouchId": "Fingerabdruck",
|
||||||
|
"setGenTouchIdDisabled": "Fingerabdruck nicht benutzen",
|
||||||
|
"setGenTouchIdMemory": "Nur mit Fingerabdruck entsperren, wenn KeeWeb aktiv ist",
|
||||||
|
"setGenTouchIdFile": "Benutze immer den Fingerabdruck anstatt das Master-Passwort",
|
||||||
|
"setGenTouchIdPass": "Benötige Master-Passwort nach",
|
||||||
"setFilePath": "Dateipfad",
|
"setFilePath": "Dateipfad",
|
||||||
"setFileStorage": "Diese Datei wird von {} geladen.",
|
"setFileStorage": "Diese Datei wird von {} geladen.",
|
||||||
"setFileIntl": "Diese Datei ist im internen App-Speicher abgelegt",
|
"setFileIntl": "Diese Datei ist im internen App-Speicher abgelegt",
|
||||||
|
|
|
@ -26,6 +26,15 @@
|
||||||
"shiftKey": "shift",
|
"shiftKey": "shift",
|
||||||
"altKey": "alt",
|
"altKey": "alt",
|
||||||
"error": "erreur",
|
"error": "erreur",
|
||||||
|
"oneMinute": "une minute",
|
||||||
|
"minutes": "{} minutes",
|
||||||
|
"oneHour": "une heure",
|
||||||
|
"hours": "{} heures",
|
||||||
|
"oneDay": "un jour",
|
||||||
|
"days": "{} jours",
|
||||||
|
"oneWeek": "une semaine",
|
||||||
|
"oneMonth": "un mois",
|
||||||
|
"oneYear": "une année",
|
||||||
"cache": "cache",
|
"cache": "cache",
|
||||||
"file": "fichier",
|
"file": "fichier",
|
||||||
"device": "appareil",
|
"device": "appareil",
|
||||||
|
@ -87,7 +96,7 @@
|
||||||
"tagExists": "Ce tag existe déjà",
|
"tagExists": "Ce tag existe déjà",
|
||||||
"tagExistsBody": "Un tag existe déjà avec ce nom. Merci de choisir un autre nom.",
|
"tagExistsBody": "Un tag existe déjà avec ce nom. Merci de choisir un autre nom.",
|
||||||
"tagBadName": "Nom invalide",
|
"tagBadName": "Nom invalide",
|
||||||
"tagBadNameBody": "Un nom de tag ne peut pas contenir les caractères {}. Merci de les supprimer.",
|
"tagBadNameBody": "Tag name cannot contain characters {}. Please remove them.",
|
||||||
"genPsTitle": "Préréglages du Générateur",
|
"genPsTitle": "Préréglages du Générateur",
|
||||||
"genPsCreate": "Nouveau préréglage",
|
"genPsCreate": "Nouveau préréglage",
|
||||||
"genPsDelete": "Supprimer préréglage",
|
"genPsDelete": "Supprimer préréglage",
|
||||||
|
@ -122,7 +131,7 @@
|
||||||
"listNoWebsite": "aucun site web",
|
"listNoWebsite": "aucun site web",
|
||||||
"listNoUser": "aucun utilisateur",
|
"listNoUser": "aucun utilisateur",
|
||||||
"listNoAttachments": "aucune pièce-jointe",
|
"listNoAttachments": "aucune pièce-jointe",
|
||||||
"listAddTemplateHeader": "Templates",
|
"listAddTemplateHeader": "Modèles",
|
||||||
"listAddTemplateBody1": "Les modèles (templates) vous permettent de créer des nouvelles entrées en un clic. Ajouter quelque chose à l'entrée du modèle et ensuite cliquer de nouveau sur {} pour utiliser ce modèle.",
|
"listAddTemplateBody1": "Les modèles (templates) vous permettent de créer des nouvelles entrées en un clic. Ajouter quelque chose à l'entrée du modèle et ensuite cliquer de nouveau sur {} pour utiliser ce modèle.",
|
||||||
"listAddTemplateBody2": "Vous pouvez toujours retrouver vos modèles dans le groupe {}.",
|
"listAddTemplateBody2": "Vous pouvez toujours retrouver vos modèles dans le groupe {}.",
|
||||||
"searchAddNew": "Ajouter Nouveau",
|
"searchAddNew": "Ajouter Nouveau",
|
||||||
|
@ -237,7 +246,7 @@
|
||||||
"detDelToTrashBody": "L'entrée sera déplacée dans la corbeille.",
|
"detDelToTrashBody": "L'entrée sera déplacée dans la corbeille.",
|
||||||
"detFieldCopied": "Copié",
|
"detFieldCopied": "Copié",
|
||||||
"detFieldCopiedTime": "Copié pendant {} secondes",
|
"detFieldCopiedTime": "Copié pendant {} secondes",
|
||||||
"detCopyHint": "Vous pouvez copier la valeur du champ en cliquant sur son titre",
|
"detCopyHint": "You can copy field value by clicking its title",
|
||||||
"detMore": "plus",
|
"detMore": "plus",
|
||||||
"detClickToAddField": "cliquez pour ajouter un nouveau champ",
|
"detClickToAddField": "cliquez pour ajouter un nouveau champ",
|
||||||
"detMenuAddNewField": "Ajouter nouveau champ",
|
"detMenuAddNewField": "Ajouter nouveau champ",
|
||||||
|
@ -286,6 +295,12 @@
|
||||||
"detRevealField": "Révéler",
|
"detRevealField": "Révéler",
|
||||||
"detHideField": "Cacher",
|
"detHideField": "Cacher",
|
||||||
"detAutoTypeField": "Saisie auto",
|
"detAutoTypeField": "Saisie auto",
|
||||||
|
"detIssuesHideTooltip": "Cacher cet avertissement",
|
||||||
|
"detIssueWeakPassword": "Ce mot de passe est faible, nous vous recommandons de le changer",
|
||||||
|
"detIssuePoorPassword": "Ce mot de passe est très faible, nous vous recommandons très fort de le changer",
|
||||||
|
"detIssuePwnedPassword": "Ce mot de passe a été exposé à une faille selon {}, il est recommandé de le changer",
|
||||||
|
"detIssuePasswordCheckError": "Une erreur est survenue en vérifiant la force du mot de passe en ligne",
|
||||||
|
"detIssueOldPassword": "Ce mot de passe est vieux",
|
||||||
"autoTypeEntryFields": "Champs",
|
"autoTypeEntryFields": "Champs",
|
||||||
"autoTypeModifiers": "Touches modificatrices",
|
"autoTypeModifiers": "Touches modificatrices",
|
||||||
"autoTypeKeys": "Clés",
|
"autoTypeKeys": "Clés",
|
||||||
|
@ -349,7 +364,7 @@
|
||||||
"setGenExtractingUpdate": "Décompression de la mise à jour...",
|
"setGenExtractingUpdate": "Décompression de la mise à jour...",
|
||||||
"setGenCheckErr": "Une erreur est intervenue durant le téléchargement de la mise à jour",
|
"setGenCheckErr": "Une erreur est intervenue durant le téléchargement de la mise à jour",
|
||||||
"setGenNeverChecked": "Ne jamais vérifier les mises à jour",
|
"setGenNeverChecked": "Ne jamais vérifier les mises à jour",
|
||||||
"setGenRestartToUpdate": "Redémarrer pour mettre à jour",
|
"setGenRestartToUpdate": "Redémarrer KeeWeb pour mettre à jour",
|
||||||
"setGenDownloadAndRestart": "Télécharger la mise à jour et redémarrer",
|
"setGenDownloadAndRestart": "Télécharger la mise à jour et redémarrer",
|
||||||
"setGenAppearance": "Apparence",
|
"setGenAppearance": "Apparence",
|
||||||
"setGenTheme": "Thème",
|
"setGenTheme": "Thème",
|
||||||
|
@ -424,6 +439,22 @@
|
||||||
"setGenShowAppLogs": "Voir les logs",
|
"setGenShowAppLogs": "Voir les logs",
|
||||||
"setGenReloadApp": "Recharger l'application",
|
"setGenReloadApp": "Recharger l'application",
|
||||||
"setGenFieldLabelDblClickAutoType": "Remplissage auto par double clic sur les noms de champ",
|
"setGenFieldLabelDblClickAutoType": "Remplissage auto par double clic sur les noms de champ",
|
||||||
|
"setGenTouchId": "Touch ID",
|
||||||
|
"setGenTouchIdDisabled": "Ne pas utiliser Touch ID",
|
||||||
|
"setGenTouchIdMemory": "Dévérouiller avec Touch ID uniquement quand Keeweb est lancé",
|
||||||
|
"setGenTouchIdFile": "Toujours utiliser Touch ID à la place du mot de passe",
|
||||||
|
"setGenTouchIdPass": "Exige le mot de passe après",
|
||||||
|
"setGenAudit": "Audit",
|
||||||
|
"setGenAuditPasswords": "Voir des avertissements sur la force du mot de passe",
|
||||||
|
"setGenAuditPasswordEntropy": "Vérifier la longueur et l'aléas du mot de passe",
|
||||||
|
"setGenExcludePinsFromAudit": "Ne jamais vérifier les codes PIN courts, comme 123456",
|
||||||
|
"setGenCheckPasswordsOnHIBP": "Vérifier les mots de passe en utilisant un service en ligne {}",
|
||||||
|
"setGenHelpHIBP": "KeeWeb peut vérifier si vos mots de passes ont été précédemment exposés à une faille de sécurité en utilisant un service en ligne. Votre mot de passe ne peut pas être récupéré à partir de données envoyées en ligne, cependant le nombre de mots de passes vérifiés peut être exposé. Plus d'informations sur votre sécurité en utilisant ce service peuvent être trouvées {}. Si cette option est activée, KeeWeb vérifiera automatiquement vos mots de passes là.",
|
||||||
|
"setGenHelpHIBPLink": "ici",
|
||||||
|
"setGenAuditPasswordAge": "Vieux mots de passe",
|
||||||
|
"setGenAuditPasswordAgeOff": "Ne pas afficher d'avertissements à propos des vieux mots de passe",
|
||||||
|
"setGenAuditPasswordAgeOneYear": "Afficher des avertissements pour les mots de passe plus vieux qu'un an",
|
||||||
|
"setGenAuditPasswordAgeYears": "Afficher des avertissement pour les mots de passe plus vieux que {} ans",
|
||||||
"setFilePath": "Chemin",
|
"setFilePath": "Chemin",
|
||||||
"setFileStorage": "Le fichier est ouvert de {}.",
|
"setFileStorage": "Le fichier est ouvert de {}.",
|
||||||
"setFileIntl": "Le fichier est conservé dans le stockage interne de l'application",
|
"setFileIntl": "Le fichier est conservé dans le stockage interne de l'application",
|
||||||
|
@ -615,7 +646,7 @@
|
||||||
"gdriveSharedWithMe": "Partagé avec moi",
|
"gdriveSharedWithMe": "Partagé avec moi",
|
||||||
"webdavSaveMethod": "Méthode de sauvegarde",
|
"webdavSaveMethod": "Méthode de sauvegarde",
|
||||||
"webdavSaveMove": "Envoyer un fichier temporaire et le déplacer",
|
"webdavSaveMove": "Envoyer un fichier temporaire et le déplacer",
|
||||||
"webdavSavePut": "Ecraser le fichier kdbx avec PUT",
|
"webdavSavePut": "Écraser le fichier kdbx avec PUT",
|
||||||
"webdavNoLastModified": "L'entête HTTP \"Last-Modified\" est absent",
|
"webdavNoLastModified": "L'entête HTTP \"Last-Modified\" est absent",
|
||||||
"webdavStatReload": "Toujours recharger le fichier au lieu de se fier à l'entête HTTP \"Last-Modified\"",
|
"webdavStatReload": "Toujours recharger le fichier au lieu de se fier à l'entête HTTP \"Last-Modified\"",
|
||||||
"launcherSave": "Sauvegarder base des mots de passe",
|
"launcherSave": "Sauvegarder base des mots de passe",
|
||||||
|
@ -640,5 +671,6 @@
|
||||||
"yubiKeyTouchRequestedBody": "Merci de toucher votre YubiKey avec le numéro de série {}",
|
"yubiKeyTouchRequestedBody": "Merci de toucher votre YubiKey avec le numéro de série {}",
|
||||||
"yubiKeyDisabledErrorHeader": "L'USB est désactivé",
|
"yubiKeyDisabledErrorHeader": "L'USB est désactivé",
|
||||||
"yubiKeyDisabledErrorBody": "Yubikey est nécessaire pour ouvrir ce fichier, merci d'activer les appareils USB dans les paramètres",
|
"yubiKeyDisabledErrorBody": "Yubikey est nécessaire pour ouvrir ce fichier, merci d'activer les appareils USB dans les paramètres",
|
||||||
"yubiKeyErrorWithCode": "Erreur Yubikey code {}."
|
"yubiKeyErrorWithCode": "Erreur Yubikey code {}.",
|
||||||
|
"bioOpenAuthPrompt": "ouvrir \"{}\""
|
||||||
}
|
}
|
|
@ -4,8 +4,8 @@ import { SearchResultCollection } from 'collections/search-result-collection';
|
||||||
import { FileCollection } from 'collections/file-collection';
|
import { FileCollection } from 'collections/file-collection';
|
||||||
import { FileInfoCollection } from 'collections/file-info-collection';
|
import { FileInfoCollection } from 'collections/file-info-collection';
|
||||||
import { RuntimeInfo } from 'const/runtime-info';
|
import { RuntimeInfo } from 'const/runtime-info';
|
||||||
import { Launcher } from 'comp/launcher';
|
|
||||||
import { UsbListener } from 'comp/app/usb-listener';
|
import { UsbListener } from 'comp/app/usb-listener';
|
||||||
|
import { NativeModules } from 'comp/launcher/native-modules';
|
||||||
import { Timeouts } from 'const/timeouts';
|
import { Timeouts } from 'const/timeouts';
|
||||||
import { AppSettingsModel } from 'models/app-settings-model';
|
import { AppSettingsModel } from 'models/app-settings-model';
|
||||||
import { EntryModel } from 'models/entry-model';
|
import { EntryModel } from 'models/entry-model';
|
||||||
|
@ -37,6 +37,7 @@ class AppModel {
|
||||||
isBeta = RuntimeInfo.beta;
|
isBeta = RuntimeInfo.beta;
|
||||||
advancedSearch = null;
|
advancedSearch = null;
|
||||||
attachedYubiKeysCount = 0;
|
attachedYubiKeysCount = 0;
|
||||||
|
memoryPasswordStorage = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
Events.on('refresh', this.refresh.bind(this));
|
Events.on('refresh', this.refresh.bind(this));
|
||||||
|
@ -525,7 +526,8 @@ class AppModel {
|
||||||
fileInfo &&
|
fileInfo &&
|
||||||
fileInfo.openDate &&
|
fileInfo.openDate &&
|
||||||
fileInfo.rev === params.rev &&
|
fileInfo.rev === params.rev &&
|
||||||
fileInfo.storage !== 'file'
|
fileInfo.storage !== 'file' &&
|
||||||
|
!this.settings.disableOfflineStorage
|
||||||
) {
|
) {
|
||||||
logger.info('Open file from cache because it is latest');
|
logger.info('Open file from cache because it is latest');
|
||||||
this.openFileFromCache(
|
this.openFileFromCache(
|
||||||
|
@ -546,7 +548,12 @@ class AppModel {
|
||||||
},
|
},
|
||||||
fileInfo
|
fileInfo
|
||||||
);
|
);
|
||||||
} else if (!fileInfo || !fileInfo.openDate || params.storage === 'file') {
|
} else if (
|
||||||
|
!fileInfo ||
|
||||||
|
!fileInfo.openDate ||
|
||||||
|
params.storage === 'file' ||
|
||||||
|
this.settings.disableOfflineStorage
|
||||||
|
) {
|
||||||
this.openFileFromStorage(params, callback, fileInfo, logger);
|
this.openFileFromStorage(params, callback, fileInfo, logger);
|
||||||
} else {
|
} else {
|
||||||
logger.info('Open file from cache, will sync after load', params.storage);
|
logger.info('Open file from cache, will sync after load', params.storage);
|
||||||
|
@ -594,7 +601,7 @@ class AppModel {
|
||||||
logger.info('Load from storage');
|
logger.info('Load from storage');
|
||||||
storage.load(params.path, params.opts, (err, data, stat) => {
|
storage.load(params.path, params.opts, (err, data, stat) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (fileInfo && fileInfo.openDate) {
|
if (fileInfo && fileInfo.openDate && !this.settings.disableOfflineStorage) {
|
||||||
logger.info('Open file from cache because of storage load error', err);
|
logger.info('Open file from cache because of storage load error', err);
|
||||||
this.openFileFromCache(params, callback, fileInfo);
|
this.openFileFromCache(params, callback, fileInfo);
|
||||||
} else {
|
} else {
|
||||||
|
@ -618,7 +625,8 @@ class AppModel {
|
||||||
!noCache &&
|
!noCache &&
|
||||||
fileInfo &&
|
fileInfo &&
|
||||||
storage.name !== 'file' &&
|
storage.name !== 'file' &&
|
||||||
(err || (stat && stat.rev === cacheRev))
|
(err || (stat && stat.rev === cacheRev)) &&
|
||||||
|
!this.settings.disableOfflineStorage
|
||||||
) {
|
) {
|
||||||
logger.info(
|
logger.info(
|
||||||
'Open file from cache because ' + (err ? 'stat error' : 'it is latest'),
|
'Open file from cache because ' + (err ? 'stat error' : 'it is latest'),
|
||||||
|
@ -663,10 +671,13 @@ class AppModel {
|
||||||
path: params.path,
|
path: params.path,
|
||||||
keyFileName: params.keyFileName,
|
keyFileName: params.keyFileName,
|
||||||
keyFilePath: params.keyFilePath,
|
keyFilePath: params.keyFilePath,
|
||||||
backup: (fileInfo && fileInfo.backup) || null,
|
backup: fileInfo?.backup || null,
|
||||||
fingerprint: (fileInfo && fileInfo.fingerprint) || null,
|
|
||||||
chalResp: params.chalResp
|
chalResp: params.chalResp
|
||||||
});
|
});
|
||||||
|
if (params.encryptedPassword) {
|
||||||
|
file.encryptedPassword = fileInfo.encryptedPassword;
|
||||||
|
file.encryptedPasswordDate = fileInfo?.encryptedPasswordDate || new Date();
|
||||||
|
}
|
||||||
const openComplete = (err) => {
|
const openComplete = (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
|
@ -685,7 +696,7 @@ class AppModel {
|
||||||
if (fileInfo) {
|
if (fileInfo) {
|
||||||
file.syncDate = fileInfo.syncDate;
|
file.syncDate = fileInfo.syncDate;
|
||||||
}
|
}
|
||||||
if (updateCacheOnSuccess) {
|
if (updateCacheOnSuccess && !this.settings.disableOfflineStorage) {
|
||||||
logger.info('Save loaded file to cache');
|
logger.info('Save loaded file to cache');
|
||||||
Storage.cache.save(file.id, null, params.fileData);
|
Storage.cache.save(file.id, null, params.fileData);
|
||||||
}
|
}
|
||||||
|
@ -755,7 +766,6 @@ class AppModel {
|
||||||
syncDate: file.syncDate || dt,
|
syncDate: file.syncDate || dt,
|
||||||
openDate: dt,
|
openDate: dt,
|
||||||
backup: file.backup,
|
backup: file.backup,
|
||||||
fingerprint: file.fingerprint,
|
|
||||||
chalResp: file.chalResp
|
chalResp: file.chalResp
|
||||||
});
|
});
|
||||||
switch (this.settings.rememberKeyFiles) {
|
switch (this.settings.rememberKeyFiles) {
|
||||||
|
@ -771,6 +781,14 @@ class AppModel {
|
||||||
keyFilePath: file.keyFilePath || null
|
keyFilePath: file.keyFilePath || null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (this.settings.deviceOwnerAuth === 'file' && file.encryptedPassword) {
|
||||||
|
const maxDate = new Date(file.encryptedPasswordDate);
|
||||||
|
maxDate.setMinutes(maxDate.getMinutes() + this.settings.deviceOwnerAuthTimeoutMinutes);
|
||||||
|
if (maxDate > new Date()) {
|
||||||
|
fileInfo.encryptedPassword = file.encryptedPassword;
|
||||||
|
fileInfo.encryptedPasswordDate = file.encryptedPasswordDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
this.fileInfos.remove(file.id);
|
this.fileInfos.remove(file.id);
|
||||||
this.fileInfos.unshift(fileInfo);
|
this.fileInfos.unshift(fileInfo);
|
||||||
this.fileInfos.save();
|
this.fileInfos.save();
|
||||||
|
@ -808,14 +826,14 @@ class AppModel {
|
||||||
if (data && backup && backup.enabled && backup.pending) {
|
if (data && backup && backup.enabled && backup.pending) {
|
||||||
this.scheduleBackupFile(file, data);
|
this.scheduleBackupFile(file, data);
|
||||||
}
|
}
|
||||||
if (params) {
|
|
||||||
this.saveFileFingerprint(file, params.password);
|
|
||||||
}
|
|
||||||
if (this.settings.yubiKeyAutoOpen) {
|
if (this.settings.yubiKeyAutoOpen) {
|
||||||
if (this.attachedYubiKeysCount > 0 && !this.files.some((f) => f.external)) {
|
if (this.attachedYubiKeysCount > 0 && !this.files.some((f) => f.external)) {
|
||||||
this.tryOpenOtpDeviceInBackground();
|
this.tryOpenOtpDeviceInBackground();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.settings.deviceOwnerAuth) {
|
||||||
|
this.saveEncryptedPassword(file, params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileClosed(file) {
|
fileClosed(file) {
|
||||||
|
@ -858,7 +876,7 @@ class AppModel {
|
||||||
path = Storage[storage].getPathForName(file.name);
|
path = Storage[storage].getPathForName(file.name);
|
||||||
}
|
}
|
||||||
const optionsForLogging = { ...options };
|
const optionsForLogging = { ...options };
|
||||||
if (optionsForLogging && optionsForLogging.opts && optionsForLogging.opts.password) {
|
if (optionsForLogging.opts && optionsForLogging.opts.password) {
|
||||||
optionsForLogging.opts = { ...optionsForLogging.opts };
|
optionsForLogging.opts = { ...optionsForLogging.opts };
|
||||||
optionsForLogging.opts.password = '***';
|
optionsForLogging.opts.password = '***';
|
||||||
}
|
}
|
||||||
|
@ -965,6 +983,10 @@ class AppModel {
|
||||||
logger.info('Updated sync date, saving modified file');
|
logger.info('Updated sync date, saving modified file');
|
||||||
saveToCacheAndStorage();
|
saveToCacheAndStorage();
|
||||||
} else if (file.dirty) {
|
} else if (file.dirty) {
|
||||||
|
if (this.settings.disableOfflineStorage) {
|
||||||
|
logger.info('File is dirty and cache is disabled');
|
||||||
|
return complete(err);
|
||||||
|
}
|
||||||
logger.info('Saving not modified dirty file to cache');
|
logger.info('Saving not modified dirty file to cache');
|
||||||
Storage.cache.save(fileInfo.id, null, data, (err) => {
|
Storage.cache.save(fileInfo.id, null, data, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -1027,6 +1049,9 @@ class AppModel {
|
||||||
} else if (!file.dirty) {
|
} else if (!file.dirty) {
|
||||||
logger.info('Saving to storage, skip cache because not dirty');
|
logger.info('Saving to storage, skip cache because not dirty');
|
||||||
saveToStorage(data);
|
saveToStorage(data);
|
||||||
|
} else if (this.settings.disableOfflineStorage) {
|
||||||
|
logger.info('Saving to storage because cache is disabled');
|
||||||
|
saveToStorage(data);
|
||||||
} else {
|
} else {
|
||||||
logger.info('Saving to cache');
|
logger.info('Saving to cache');
|
||||||
Storage.cache.save(fileInfo.id, null, data, (err) => {
|
Storage.cache.save(fileInfo.id, null, data, (err) => {
|
||||||
|
@ -1050,6 +1075,10 @@ class AppModel {
|
||||||
logger.info('File does not exist in storage, creating');
|
logger.info('File does not exist in storage, creating');
|
||||||
saveToCacheAndStorage();
|
saveToCacheAndStorage();
|
||||||
} else if (file.dirty) {
|
} else if (file.dirty) {
|
||||||
|
if (this.settings.disableOfflineStorage) {
|
||||||
|
logger.info('Stat error, dirty, cache is disabled', err || 'no error');
|
||||||
|
return complete(err);
|
||||||
|
}
|
||||||
logger.info('Stat error, dirty, save to cache', err || 'no error');
|
logger.info('Stat error, dirty, save to cache', err || 'no error');
|
||||||
file.getData((data, e) => {
|
file.getData((data, e) => {
|
||||||
if (e) {
|
if (e) {
|
||||||
|
@ -1087,6 +1116,14 @@ class AppModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteAllCachedFiles() {
|
||||||
|
for (const fileInfo of this.fileInfos) {
|
||||||
|
if (fileInfo.storage && !fileInfo.modified) {
|
||||||
|
Storage.cache.remove(fileInfo.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
clearStoredKeyFiles() {
|
clearStoredKeyFiles() {
|
||||||
for (const fileInfo of this.fileInfos) {
|
for (const fileInfo of this.fileInfos) {
|
||||||
fileInfo.set({
|
fileInfo.set({
|
||||||
|
@ -1224,18 +1261,6 @@ class AppModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveFileFingerprint(file, password) {
|
|
||||||
if (Launcher && Launcher.fingerprints && !file.fingerprint) {
|
|
||||||
const fileInfo = this.fileInfos.get(file.id);
|
|
||||||
Launcher.fingerprints.register(file.id, password, (token) => {
|
|
||||||
if (token) {
|
|
||||||
fileInfo.fingerprint = token;
|
|
||||||
this.fileInfos.save();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
usbDevicesChanged() {
|
usbDevicesChanged() {
|
||||||
const attachedYubiKeysCount = this.attachedYubiKeysCount;
|
const attachedYubiKeysCount = this.attachedYubiKeysCount;
|
||||||
|
|
||||||
|
@ -1286,6 +1311,97 @@ class AppModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveEncryptedPassword(file, params) {
|
||||||
|
if (!this.settings.deviceOwnerAuth || params.encryptedPassword) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NativeModules.hardwareEncrypt(params.password)
|
||||||
|
.then((encryptedPassword) => {
|
||||||
|
encryptedPassword = encryptedPassword.toBase64();
|
||||||
|
const fileInfo = this.fileInfos.get(file.id);
|
||||||
|
const encryptedPasswordDate = new Date();
|
||||||
|
file.encryptedPassword = encryptedPassword;
|
||||||
|
file.encryptedPasswordDate = encryptedPasswordDate;
|
||||||
|
if (this.settings.deviceOwnerAuth === 'file') {
|
||||||
|
fileInfo.encryptedPassword = encryptedPassword;
|
||||||
|
fileInfo.encryptedPasswordDate = encryptedPasswordDate;
|
||||||
|
this.fileInfos.save();
|
||||||
|
} else if (this.settings.deviceOwnerAuth === 'memory') {
|
||||||
|
this.memoryPasswordStorage[file.id] = {
|
||||||
|
value: encryptedPassword,
|
||||||
|
date: encryptedPasswordDate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
file.encryptedPassword = null;
|
||||||
|
file.encryptedPasswordDate = null;
|
||||||
|
delete this.memoryPasswordStorage[file.id];
|
||||||
|
this.appLogger.error('Error encrypting password', e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getMemoryPassword(fileId) {
|
||||||
|
return this.memoryPasswordStorage[fileId];
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEncryptedPasswordsStorage() {
|
||||||
|
if (this.settings.deviceOwnerAuth === 'file') {
|
||||||
|
let changed = false;
|
||||||
|
for (const fileInfo of this.fileInfos) {
|
||||||
|
if (this.memoryPasswordStorage[fileInfo.id]) {
|
||||||
|
fileInfo.encryptedPassword = this.memoryPasswordStorage[fileInfo.id].value;
|
||||||
|
fileInfo.encryptedPasswordDate = this.memoryPasswordStorage[fileInfo.id].date;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
this.fileInfos.save();
|
||||||
|
}
|
||||||
|
for (const file of this.files) {
|
||||||
|
if (this.memoryPasswordStorage[file.id]) {
|
||||||
|
file.encryptedPassword = this.memoryPasswordStorage[file.id].value;
|
||||||
|
file.encryptedPasswordDate = this.memoryPasswordStorage[file.id].date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.settings.deviceOwnerAuth === 'memory') {
|
||||||
|
let changed = false;
|
||||||
|
for (const fileInfo of this.fileInfos) {
|
||||||
|
if (fileInfo.encryptedPassword) {
|
||||||
|
this.memoryPasswordStorage[fileInfo.id] = {
|
||||||
|
value: fileInfo.encryptedPassword,
|
||||||
|
date: fileInfo.encryptedPasswordDate
|
||||||
|
};
|
||||||
|
fileInfo.encryptedPassword = null;
|
||||||
|
fileInfo.encryptedPasswordDate = null;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
this.fileInfos.save();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let changed = false;
|
||||||
|
for (const fileInfo of this.fileInfos) {
|
||||||
|
if (fileInfo.encryptedPassword) {
|
||||||
|
fileInfo.encryptedPassword = null;
|
||||||
|
fileInfo.encryptedPasswordDate = null;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
this.fileInfos.save();
|
||||||
|
}
|
||||||
|
for (const file of this.files) {
|
||||||
|
if (file.encryptedPassword) {
|
||||||
|
file.encryptedPassword = null;
|
||||||
|
file.encryptedPasswordDate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.memoryPasswordStorage = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { AppModel };
|
export { AppModel };
|
||||||
|
|
|
@ -339,7 +339,7 @@ class EntryModel extends Model {
|
||||||
if (val && !val.isProtected) {
|
if (val && !val.isProtected) {
|
||||||
// https://github.com/keeweb/keeweb/issues/910
|
// https://github.com/keeweb/keeweb/issues/910
|
||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
val = val.replace(/[\x00-\x09\x0B-\x0C\x0E-\x1F\uFFF0-\uFFFF]/g, '');
|
val = val.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\uFFF0-\uFFFF]/g, '');
|
||||||
}
|
}
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
@ -627,6 +627,18 @@ class EntryModel extends Model {
|
||||||
return KdbxToHtml.entryToHtml(this.file.db, this.entry);
|
return KdbxToHtml.entryToHtml(this.file.db, this.entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canCheckPasswordIssues() {
|
||||||
|
return !this.entry.customData?.IgnorePwIssues;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIgnorePasswordIssues() {
|
||||||
|
if (!this.entry.customData) {
|
||||||
|
this.entry.customData = {};
|
||||||
|
}
|
||||||
|
this.entry.customData.IgnorePwIssues = '1';
|
||||||
|
this._entryModified();
|
||||||
|
}
|
||||||
|
|
||||||
static fromEntry(entry, group, file) {
|
static fromEntry(entry, group, file) {
|
||||||
const model = new EntryModel();
|
const model = new EntryModel();
|
||||||
model.setEntry(entry, group, file);
|
model.setEntry(entry, group, file);
|
||||||
|
|
|
@ -16,8 +16,10 @@ const DefaultProperties = {
|
||||||
keyFilePath: null,
|
keyFilePath: null,
|
||||||
opts: null,
|
opts: null,
|
||||||
backup: null,
|
backup: null,
|
||||||
fingerprint: null,
|
fingerprint: null, // obsolete
|
||||||
chalResp: null
|
chalResp: null,
|
||||||
|
encryptedPassword: null,
|
||||||
|
encryptedPasswordDate: null
|
||||||
};
|
};
|
||||||
|
|
||||||
class FileInfoModel extends Model {
|
class FileInfoModel extends Model {
|
||||||
|
|
|
@ -487,9 +487,11 @@ class FileModel extends Model {
|
||||||
syncError: error
|
syncError: error
|
||||||
});
|
});
|
||||||
|
|
||||||
const shouldResetFingerprint = this.passwordChanged && this.fingerprint;
|
if (!error && this.passwordChanged && this.encryptedPassword) {
|
||||||
if (shouldResetFingerprint && !error) {
|
this.set({
|
||||||
this.fingerprint = null;
|
encryptedPassword: null,
|
||||||
|
encryptedPasswordDate: null
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.open) {
|
if (!this.open) {
|
||||||
|
@ -759,10 +761,12 @@ FileModel.defineModelProperties({
|
||||||
keyEncryptionRounds: null,
|
keyEncryptionRounds: null,
|
||||||
kdfName: null,
|
kdfName: null,
|
||||||
kdfParameters: null,
|
kdfParameters: null,
|
||||||
fingerprint: null,
|
fingerprint: null, // obsolete
|
||||||
oldPasswordHash: null,
|
oldPasswordHash: null,
|
||||||
oldKeyFileHash: null,
|
oldKeyFileHash: null,
|
||||||
oldKeyChangeDate: null
|
oldKeyChangeDate: null,
|
||||||
|
encryptedPassword: null,
|
||||||
|
encryptedPasswordDate: null
|
||||||
});
|
});
|
||||||
|
|
||||||
export { FileModel };
|
export { FileModel };
|
||||||
|
|
|
@ -77,36 +77,37 @@ class MenuModel extends Model {
|
||||||
locTitle: 'setGenAppearance',
|
locTitle: 'setGenAppearance',
|
||||||
icon: '0',
|
icon: '0',
|
||||||
page: 'general',
|
page: 'general',
|
||||||
section: 'appearance',
|
section: 'appearance'
|
||||||
active: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
locTitle: 'setGenFunction',
|
locTitle: 'setGenFunction',
|
||||||
icon: '0',
|
icon: '0',
|
||||||
page: 'general',
|
page: 'general',
|
||||||
section: 'function',
|
section: 'function'
|
||||||
active: true
|
},
|
||||||
|
{
|
||||||
|
locTitle: 'setGenAudit',
|
||||||
|
icon: '0',
|
||||||
|
page: 'general',
|
||||||
|
section: 'audit'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
locTitle: 'setGenLock',
|
locTitle: 'setGenLock',
|
||||||
icon: '0',
|
icon: '0',
|
||||||
page: 'general',
|
page: 'general',
|
||||||
section: 'lock',
|
section: 'lock'
|
||||||
active: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
locTitle: 'setGenStorage',
|
locTitle: 'setGenStorage',
|
||||||
icon: '0',
|
icon: '0',
|
||||||
page: 'general',
|
page: 'general',
|
||||||
section: 'storage',
|
section: 'storage'
|
||||||
active: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
locTitle: 'advanced',
|
locTitle: 'advanced',
|
||||||
icon: '0',
|
icon: '0',
|
||||||
page: 'general',
|
page: 'general',
|
||||||
section: 'advanced',
|
section: 'advanced'
|
||||||
active: true
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
this.shortcutsSection = new MenuSectionModel([
|
this.shortcutsSection = new MenuSectionModel([
|
||||||
|
|
|
@ -27,7 +27,7 @@ class MenuSectionModel extends Model {
|
||||||
removeByFile(file) {
|
removeByFile(file) {
|
||||||
const items = this.items;
|
const items = this.items;
|
||||||
items.find((item) => {
|
items.find((item) => {
|
||||||
if (item.file === file || item.file === file) {
|
if (item.file === file) {
|
||||||
items.remove(item);
|
items.remove(item);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ class MenuSectionModel extends Model {
|
||||||
replaceByFile(file, newItem) {
|
replaceByFile(file, newItem) {
|
||||||
const items = this.items;
|
const items = this.items;
|
||||||
items.find((item, ix) => {
|
items.find((item, ix) => {
|
||||||
if (item.file === file || item.file === file) {
|
if (item.file === file) {
|
||||||
items[ix] = newItem;
|
items[ix] = newItem;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -209,6 +209,10 @@ class StorageWebDav extends StorageBase {
|
||||||
.replace(/[^/]*$/, movePath);
|
.replace(/[^/]*$/, movePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// prevent double encoding, see #1729
|
||||||
|
const encodedMovePath = /%[A-Z0-9]{2}/.test(movePath)
|
||||||
|
? movePath
|
||||||
|
: encodeURI(movePath);
|
||||||
this._request(
|
this._request(
|
||||||
{
|
{
|
||||||
...saveOpts,
|
...saveOpts,
|
||||||
|
@ -217,7 +221,7 @@ class StorageWebDav extends StorageBase {
|
||||||
path: tmpPath,
|
path: tmpPath,
|
||||||
nostat: true,
|
nostat: true,
|
||||||
headers: {
|
headers: {
|
||||||
Destination: encodeURI(movePath),
|
Destination: encodedMovePath,
|
||||||
'Overwrite': 'T'
|
'Overwrite': 'T'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import { Logger } from 'util/logger';
|
import { Logger } from 'util/logger';
|
||||||
import { Launcher } from 'comp/launcher';
|
import { Launcher } from 'comp/launcher';
|
||||||
import { Locale } from 'util/locale';
|
import { KeeWebLogo } from 'const/inline-images';
|
||||||
|
import oauthPageTemplate from 'templates/oauth/complete.hbs';
|
||||||
|
|
||||||
const DefaultPort = 48149;
|
const DefaultPort = 48149;
|
||||||
const logger = new Logger('storage-oauth-listener');
|
const logger = new Logger('storage-oauth-listener');
|
||||||
|
@ -23,9 +24,9 @@ const StorageOAuthListener = {
|
||||||
let resultHandled = false;
|
let resultHandled = false;
|
||||||
const server = http.createServer((req, resp) => {
|
const server = http.createServer((req, resp) => {
|
||||||
resp.writeHead(200, 'OK', {
|
resp.writeHead(200, 'OK', {
|
||||||
'Content-Type': 'text/plain; charset=UTF-8'
|
'Content-Type': 'text/html; charset=UTF-8'
|
||||||
});
|
});
|
||||||
resp.end(Locale.appBrowserAuthComplete);
|
resp.end(oauthPageTemplate({ logoSrc: KeeWebLogo }));
|
||||||
if (!resultHandled) {
|
if (!resultHandled) {
|
||||||
this.stop();
|
this.stop();
|
||||||
this.handleResult(req.url, listener);
|
this.handleResult(req.url, listener);
|
||||||
|
|
|
@ -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,
|
isLocal: location.origin.indexOf('localhost') >= 0,
|
||||||
|
|
||||||
supportsTitleBarStyles() {
|
supportsTitleBarStyles() {
|
||||||
return this.isMac;
|
return isDesktop && (this.isMac || this.isWindows);
|
||||||
|
},
|
||||||
|
supportsCustomTitleBarAndDraggableWindow() {
|
||||||
|
return isDesktop && this.isMac;
|
||||||
|
},
|
||||||
|
renderCustomTitleBar() {
|
||||||
|
return isDesktop && this.isWindows;
|
||||||
},
|
},
|
||||||
hasUnicodeFlags() {
|
hasUnicodeFlags() {
|
||||||
return this.isMac;
|
return this.isMac;
|
||||||
|
|
|
@ -75,3 +75,7 @@ export function isEqual(a, b) {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function minmax(val, min, max) {
|
||||||
|
return Math.min(max, Math.max(min, val));
|
||||||
|
}
|
||||||
|
|
|
@ -29,6 +29,10 @@ const StringFormat = {
|
||||||
|
|
||||||
pascalCase(str) {
|
pascalCase(str) {
|
||||||
return this.capFirst(str.replace(this.camelCaseRegex, (match) => match[1].toUpperCase()));
|
return this.capFirst(str.replace(this.camelCaseRegex, (match) => match[1].toUpperCase()));
|
||||||
|
},
|
||||||
|
|
||||||
|
replaceVersion(str, replacement) {
|
||||||
|
return str.replace(/\d+\.\d+\.\d+/g, replacement);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -34,8 +34,8 @@ const KdbxwebInit = {
|
||||||
hash(args) {
|
hash(args) {
|
||||||
const ts = logger.ts();
|
const ts = logger.ts();
|
||||||
|
|
||||||
const password = makeXoredValue(args.password);
|
const password = kdbxweb.ProtectedValue.fromBinary(args.password).dataAndSalt();
|
||||||
const salt = makeXoredValue(args.salt);
|
const salt = kdbxweb.ProtectedValue.fromBinary(args.salt).dataAndSalt();
|
||||||
|
|
||||||
return NativeModules.argon2(password, salt, {
|
return NativeModules.argon2(password, salt, {
|
||||||
type: args.type,
|
type: args.type,
|
||||||
|
@ -52,7 +52,8 @@ const KdbxwebInit = {
|
||||||
|
|
||||||
logger.debug('Argon2 hash calculated', logger.ts(ts));
|
logger.debug('Argon2 hash calculated', logger.ts(ts));
|
||||||
|
|
||||||
return readXoredValue(res);
|
res = new kdbxweb.ProtectedValue(res.data, res.salt);
|
||||||
|
return res.getBinary();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
password.data.fill(0);
|
password.data.fill(0);
|
||||||
|
@ -61,36 +62,6 @@ const KdbxwebInit = {
|
||||||
logger.error('Argon2 error', err);
|
logger.error('Argon2 error', err);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
function makeXoredValue(val) {
|
|
||||||
const data = Buffer.from(val);
|
|
||||||
const random = Buffer.from(kdbxweb.Random.getBytes(data.length));
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
data[i] ^= random[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = { data: [...data], random: [...random] };
|
|
||||||
|
|
||||||
data.fill(0);
|
|
||||||
random.fill(0);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readXoredValue(val) {
|
|
||||||
const data = Buffer.from(val.data);
|
|
||||||
const random = Buffer.from(val.random);
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
data[i] ^= random[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
val.data.fill(0);
|
|
||||||
val.random.fill(0);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return Promise.resolve(this.runtimeModule);
|
return Promise.resolve(this.runtimeModule);
|
||||||
|
|
|
@ -139,8 +139,6 @@ kdbxweb.ProtectedValue.prototype.indexOfSelfInLower = function (targetLower) {
|
||||||
return firstCharIndex;
|
return firstCharIndex;
|
||||||
};
|
};
|
||||||
|
|
||||||
window.PV = kdbxweb.ProtectedValue;
|
|
||||||
|
|
||||||
kdbxweb.ProtectedValue.prototype.equals = function (other) {
|
kdbxweb.ProtectedValue.prototype.equals = function (other) {
|
||||||
if (!other) {
|
if (!other) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -176,3 +174,38 @@ kdbxweb.ProtectedValue.prototype.isFieldReference = function () {
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RandomSalt = kdbxweb.Random.getBytes(128);
|
||||||
|
|
||||||
|
kdbxweb.ProtectedValue.prototype.saltedValue = function () {
|
||||||
|
if (!this.byteLength) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const value = this._value;
|
||||||
|
const salt = this._salt;
|
||||||
|
let salted = '';
|
||||||
|
for (let i = 0, len = value.length; i < len; i++) {
|
||||||
|
const byte = value[i] ^ salt[i];
|
||||||
|
salted += String.fromCharCode(byte ^ RandomSalt[i % RandomSalt.length]);
|
||||||
|
}
|
||||||
|
return salted;
|
||||||
|
};
|
||||||
|
|
||||||
|
kdbxweb.ProtectedValue.prototype.dataAndSalt = function () {
|
||||||
|
return {
|
||||||
|
data: [...this._value],
|
||||||
|
salt: [...this._salt]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
kdbxweb.ProtectedValue.prototype.toBase64 = function () {
|
||||||
|
const binary = this.getBinary();
|
||||||
|
const base64 = kdbxweb.ByteUtils.bytesToBase64(binary);
|
||||||
|
kdbxweb.ByteUtils.zeroBuffer(binary);
|
||||||
|
return base64;
|
||||||
|
};
|
||||||
|
|
||||||
|
kdbxweb.ProtectedValue.fromBase64 = function (base64) {
|
||||||
|
const bytes = kdbxweb.ByteUtils.base64ToBytes(base64);
|
||||||
|
return kdbxweb.ProtectedValue.fromBinary(bytes);
|
||||||
|
};
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { OpenView } from 'views/open-view';
|
||||||
import { SettingsView } from 'views/settings/settings-view';
|
import { SettingsView } from 'views/settings/settings-view';
|
||||||
import { TagView } from 'views/tag-view';
|
import { TagView } from 'views/tag-view';
|
||||||
import { ImportCsvView } from 'views/import-csv-view';
|
import { ImportCsvView } from 'views/import-csv-view';
|
||||||
|
import { TitlebarView } from 'views/titlebar-view';
|
||||||
import template from 'templates/app.hbs';
|
import template from 'templates/app.hbs';
|
||||||
|
|
||||||
class AppView extends View {
|
class AppView extends View {
|
||||||
|
@ -45,6 +46,9 @@ class AppView extends View {
|
||||||
|
|
||||||
constructor(model) {
|
constructor(model) {
|
||||||
super(model);
|
super(model);
|
||||||
|
|
||||||
|
this.titlebarStyle = this.model.settings.titlebarStyle;
|
||||||
|
|
||||||
this.views.menu = new MenuView(this.model.menu, { ownParent: true });
|
this.views.menu = new MenuView(this.model.menu, { ownParent: true });
|
||||||
this.views.menuDrag = new DragView('x', { parent: '.app__menu-drag' });
|
this.views.menuDrag = new DragView('x', { parent: '.app__menu-drag' });
|
||||||
this.views.footer = new FooterView(this.model, { ownParent: true });
|
this.views.footer = new FooterView(this.model, { ownParent: true });
|
||||||
|
@ -54,12 +58,13 @@ class AppView extends View {
|
||||||
this.views.list.dragView = this.views.listDrag;
|
this.views.list.dragView = this.views.listDrag;
|
||||||
this.views.details = new DetailsView(undefined, { ownParent: true });
|
this.views.details = new DetailsView(undefined, { ownParent: true });
|
||||||
this.views.details.appModel = this.model;
|
this.views.details.appModel = this.model;
|
||||||
|
if (this.titlebarStyle !== 'default' && Features.renderCustomTitleBar()) {
|
||||||
|
this.views.titlebar = new TitlebarView(this.model);
|
||||||
|
}
|
||||||
|
|
||||||
this.views.menu.listenDrag(this.views.menuDrag);
|
this.views.menu.listenDrag(this.views.menuDrag);
|
||||||
this.views.list.listenDrag(this.views.listDrag);
|
this.views.list.listenDrag(this.views.listDrag);
|
||||||
|
|
||||||
this.titlebarStyle = this.model.settings.titlebarStyle;
|
|
||||||
|
|
||||||
this.listenTo(this.model.settings, 'change:theme', this.setTheme);
|
this.listenTo(this.model.settings, 'change:theme', this.setTheme);
|
||||||
this.listenTo(this.model.settings, 'change:locale', this.setLocale);
|
this.listenTo(this.model.settings, 'change:locale', this.setLocale);
|
||||||
this.listenTo(this.model.settings, 'change:fontSize', this.setFontSize);
|
this.listenTo(this.model.settings, 'change:fontSize', this.setFontSize);
|
||||||
|
@ -120,6 +125,9 @@ class AppView extends View {
|
||||||
}
|
}
|
||||||
if (this.titlebarStyle !== 'default') {
|
if (this.titlebarStyle !== 'default') {
|
||||||
document.body.classList.add('titlebar-' + this.titlebarStyle);
|
document.body.classList.add('titlebar-' + this.titlebarStyle);
|
||||||
|
if (Features.renderCustomTitleBar()) {
|
||||||
|
document.body.classList.add('titlebar-custom');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (Features.isMobile) {
|
if (Features.isMobile) {
|
||||||
document.body.classList.add('mobile');
|
document.body.classList.add('mobile');
|
||||||
|
@ -129,7 +137,8 @@ class AppView extends View {
|
||||||
render() {
|
render() {
|
||||||
super.render({
|
super.render({
|
||||||
beta: this.model.isBeta,
|
beta: this.model.isBeta,
|
||||||
titlebarStyle: this.titlebarStyle
|
titlebarStyle: this.titlebarStyle,
|
||||||
|
customTitlebar: Features.renderCustomTitleBar()
|
||||||
});
|
});
|
||||||
this.panelEl = this.$el.find('.app__panel:first');
|
this.panelEl = this.$el.find('.app__panel:first');
|
||||||
this.views.listWrap.render();
|
this.views.listWrap.render();
|
||||||
|
@ -139,13 +148,14 @@ class AppView extends View {
|
||||||
this.views.list.render();
|
this.views.list.render();
|
||||||
this.views.listDrag.render();
|
this.views.listDrag.render();
|
||||||
this.views.details.render();
|
this.views.details.render();
|
||||||
|
this.views.titlebar?.render();
|
||||||
this.showLastOpenFile();
|
this.showLastOpenFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
showOpenFile() {
|
showOpenFile() {
|
||||||
this.hideContextMenu();
|
this.hideContextMenu();
|
||||||
this.views.menu.hide();
|
this.views.menu.hide();
|
||||||
this.views.menuDrag.hide();
|
this.views.menuDrag.$el.parent().hide();
|
||||||
this.views.listWrap.hide();
|
this.views.listWrap.hide();
|
||||||
this.views.list.hide();
|
this.views.list.hide();
|
||||||
this.views.listDrag.hide();
|
this.views.listDrag.hide();
|
||||||
|
@ -190,7 +200,7 @@ class AppView extends View {
|
||||||
|
|
||||||
showEntries() {
|
showEntries() {
|
||||||
this.views.menu.show();
|
this.views.menu.show();
|
||||||
this.views.menuDrag.show();
|
this.views.menuDrag.$el.parent().show();
|
||||||
this.views.listWrap.show();
|
this.views.listWrap.show();
|
||||||
this.views.list.show();
|
this.views.list.show();
|
||||||
this.views.listDrag.show();
|
this.views.listDrag.show();
|
||||||
|
@ -254,7 +264,7 @@ class AppView extends View {
|
||||||
showSettings(selectedMenuItem) {
|
showSettings(selectedMenuItem) {
|
||||||
this.model.menu.setMenu('settings');
|
this.model.menu.setMenu('settings');
|
||||||
this.views.menu.show();
|
this.views.menu.show();
|
||||||
this.views.menuDrag.show();
|
this.views.menuDrag.$el.parent().show();
|
||||||
this.views.listWrap.hide();
|
this.views.listWrap.hide();
|
||||||
this.views.list.hide();
|
this.views.list.hide();
|
||||||
this.views.listDrag.hide();
|
this.views.listDrag.hide();
|
||||||
|
@ -578,9 +588,13 @@ class AppView extends View {
|
||||||
complete: (res) => {
|
complete: (res) => {
|
||||||
if (res === 'ignore') {
|
if (res === 'ignore') {
|
||||||
this.model.closeAllFiles();
|
this.model.closeAllFiles();
|
||||||
complete(true);
|
if (complete) {
|
||||||
|
complete(true);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
complete(false);
|
if (complete) {
|
||||||
|
complete(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -672,10 +686,14 @@ class AppView extends View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSettings(page) {
|
toggleSettings(page, section) {
|
||||||
let menuItem = page ? this.model.menu[page + 'Section'] : null;
|
let menuItem = page ? this.model.menu[page + 'Section'] : null;
|
||||||
if (menuItem) {
|
if (menuItem) {
|
||||||
menuItem = menuItem.items[0];
|
if (section) {
|
||||||
|
menuItem = menuItem.items.find((it) => it.section === section) || menuItem.items[0];
|
||||||
|
} else {
|
||||||
|
menuItem = menuItem.items[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this.views.settings) {
|
if (this.views.settings) {
|
||||||
if (this.views.settings.page === page || !menuItem) {
|
if (this.views.settings.page === page || !menuItem) {
|
||||||
|
@ -686,9 +704,7 @@ class AppView extends View {
|
||||||
this.views.open.toggleMore();
|
this.views.open.toggleMore();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (menuItem) {
|
this.model.menu.select({ item: menuItem });
|
||||||
this.model.menu.select({ item: menuItem });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.showSettings();
|
this.showSettings();
|
||||||
|
|
|
@ -232,7 +232,7 @@ class AutoTypeSelectView extends View {
|
||||||
this.highlightActive();
|
this.highlightActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
const view = new DropdownView();
|
const view = new DropdownView({ selectedOption: 0 });
|
||||||
this.listenTo(view, 'cancel', this.hideItemOptionsDropdown);
|
this.listenTo(view, 'cancel', this.hideItemOptionsDropdown);
|
||||||
this.listenTo(view, 'select', this.itemOptionsDropdownSelect);
|
this.listenTo(view, 'select', this.itemOptionsDropdownSelect);
|
||||||
|
|
||||||
|
|
|
@ -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 { DetailsAttachmentView } from 'views/details/details-attachment-view';
|
||||||
import { DetailsAutoTypeView } from 'views/details/details-auto-type-view';
|
import { DetailsAutoTypeView } from 'views/details/details-auto-type-view';
|
||||||
import { DetailsHistoryView } from 'views/details/details-history-view';
|
import { DetailsHistoryView } from 'views/details/details-history-view';
|
||||||
|
import { DetailsIssuesView } from 'views/details/details-issues-view';
|
||||||
import { DropdownView } from 'views/dropdown-view';
|
import { DropdownView } from 'views/dropdown-view';
|
||||||
import { createDetailsFields } from 'views/details/details-fields';
|
import { createDetailsFields } from 'views/details/details-fields';
|
||||||
import { FieldViewCustom } from 'views/fields/field-view-custom';
|
import { FieldViewCustom } from 'views/fields/field-view-custom';
|
||||||
|
@ -28,6 +29,7 @@ import { isEqual } from 'util/fn';
|
||||||
import template from 'templates/details/details.hbs';
|
import template from 'templates/details/details.hbs';
|
||||||
import emptyTemplate from 'templates/details/details-empty.hbs';
|
import emptyTemplate from 'templates/details/details-empty.hbs';
|
||||||
import groupTemplate from 'templates/details/details-group.hbs';
|
import groupTemplate from 'templates/details/details-group.hbs';
|
||||||
|
import { Launcher } from 'comp/launcher';
|
||||||
|
|
||||||
class DetailsView extends View {
|
class DetailsView extends View {
|
||||||
parent = '.app__details';
|
parent = '.app__details';
|
||||||
|
@ -117,11 +119,15 @@ class DetailsView extends View {
|
||||||
super.render();
|
super.render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const model = { deleted: this.appModel.filter.trash, ...this.model };
|
const model = {
|
||||||
|
deleted: this.appModel.filter.trash,
|
||||||
|
...this.model
|
||||||
|
};
|
||||||
this.template = template;
|
this.template = template;
|
||||||
super.render(model);
|
super.render(model);
|
||||||
this.setSelectedColor(this.model.color);
|
this.setSelectedColor(this.model.color);
|
||||||
this.addFieldViews();
|
this.addFieldViews();
|
||||||
|
this.checkPasswordIssues();
|
||||||
this.createScroll({
|
this.createScroll({
|
||||||
root: this.$el.find('.details__body')[0],
|
root: this.$el.find('.details__body')[0],
|
||||||
scroller: this.$el.find('.scroller')[0],
|
scroller: this.$el.find('.scroller')[0],
|
||||||
|
@ -152,7 +158,7 @@ class DetailsView extends View {
|
||||||
fieldView.parent = views === fieldViews ? fieldsMainEl[0] : fieldsAsideEl[0];
|
fieldView.parent = views === fieldViews ? fieldsMainEl[0] : fieldsAsideEl[0];
|
||||||
fieldView.render();
|
fieldView.render();
|
||||||
fieldView.on('change', this.fieldChanged.bind(this));
|
fieldView.on('change', this.fieldChanged.bind(this));
|
||||||
fieldView.on('copy', this.fieldCopied.bind(this));
|
fieldView.on('copy', (e) => this.copyFieldValue(e));
|
||||||
fieldView.on('autotype', (e) => this.autoType(e.source.model.sequence));
|
fieldView.on('autotype', (e) => this.autoType(e.source.model.sequence));
|
||||||
if (hideEmptyFields) {
|
if (hideEmptyFields) {
|
||||||
const value = fieldView.model.value();
|
const value = fieldView.model.value();
|
||||||
|
@ -479,13 +485,17 @@ class DetailsView extends View {
|
||||||
CopyPaste.createHiddenInput(fieldText);
|
CopyPaste.createHiddenInput(fieldText);
|
||||||
}
|
}
|
||||||
const copyRes = CopyPaste.copy(fieldText);
|
const copyRes = CopyPaste.copy(fieldText);
|
||||||
this.fieldCopied({ source: editView, copyRes });
|
this.copyFieldValue({ source: editView, copyRes });
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
copyPasswordFromShortcut(e) {
|
copyPasswordFromShortcut(e) {
|
||||||
|
if (!this.model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.model.external) {
|
if (this.model.external) {
|
||||||
this.copyOtp();
|
this.copyOtp();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -576,6 +586,9 @@ class DetailsView extends View {
|
||||||
} else if (fieldName) {
|
} else if (fieldName) {
|
||||||
this.model.setField(fieldName, e.val);
|
this.model.setField(fieldName, e.val);
|
||||||
}
|
}
|
||||||
|
if (fieldName === 'Password' && this.views.issues) {
|
||||||
|
this.views.issues.passwordChanged();
|
||||||
|
}
|
||||||
} else if (e.field === 'Tags') {
|
} else if (e.field === 'Tags') {
|
||||||
this.model.setTags(e.val);
|
this.model.setTags(e.val);
|
||||||
this.appModel.updateTags();
|
this.appModel.updateTags();
|
||||||
|
@ -988,6 +1001,20 @@ class DetailsView extends View {
|
||||||
Events.emit('auto-type', { entry, sequence });
|
Events.emit('auto-type', { entry, sequence });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkPasswordIssues() {
|
||||||
|
if (!this.model.readOnly) {
|
||||||
|
this.views.issues = new DetailsIssuesView(this.model);
|
||||||
|
this.views.issues.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copyFieldValue(e) {
|
||||||
|
this.fieldCopied(e);
|
||||||
|
if (AppSettingsModel.minimizeOnFieldCopy) {
|
||||||
|
Launcher.minimizeApp();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(DetailsView.prototype, Scrollable);
|
Object.assign(DetailsView.prototype, Scrollable);
|
||||||
|
|
|
@ -31,6 +31,8 @@ class DropdownView extends View {
|
||||||
this.once('remove', () => {
|
this.once('remove', () => {
|
||||||
$('body').off('click contextmenu keydown', this.bodyClick);
|
$('body').off('click contextmenu keydown', this.bodyClick);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.selectedOption = model?.selectedOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(config) {
|
render(config) {
|
||||||
|
@ -47,6 +49,9 @@ class DropdownView extends View {
|
||||||
top = Math.max(0, bodyRect.bottom - ownRect.height);
|
top = Math.max(0, bodyRect.bottom - ownRect.height);
|
||||||
}
|
}
|
||||||
this.$el.css({ top, left });
|
this.$el.css({ top, left });
|
||||||
|
if (typeof this.selectedOption === 'number') {
|
||||||
|
this.renderSelectedOption();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyClick(e) {
|
bodyClick(e) {
|
||||||
|
|
|
@ -64,7 +64,7 @@ class FieldViewDate extends FieldViewText {
|
||||||
this.picker = null;
|
this.picker = null;
|
||||||
}
|
}
|
||||||
newVal = new Date(newVal);
|
newVal = new Date(newVal);
|
||||||
if (!newVal || isNaN(newVal.getTime())) {
|
if (isNaN(newVal.getTime())) {
|
||||||
newVal = null;
|
newVal = null;
|
||||||
}
|
}
|
||||||
super.endEdit(newVal, extra);
|
super.endEdit(newVal, extra);
|
||||||
|
|
|
@ -104,7 +104,7 @@ class FieldViewOtp extends FieldViewText {
|
||||||
this.resetOtp();
|
this.resetOtp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.otpValue = pass || '';
|
this.otpValue = pass;
|
||||||
this.otpTimeLeft = timeLeft || 0;
|
this.otpTimeLeft = timeLeft || 0;
|
||||||
this.otpValidUntil = Date.now() + timeLeft;
|
this.otpValidUntil = Date.now() + timeLeft;
|
||||||
if (!this.editing) {
|
if (!this.editing) {
|
||||||
|
|
|
@ -21,11 +21,14 @@ class IconSelectView extends View {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const customIcons = this.model.file.getCustomIcons();
|
||||||
|
const hasCustomIcons = Object.keys(customIcons).length > 0;
|
||||||
super.render({
|
super.render({
|
||||||
sel: this.model.iconId,
|
sel: this.model.iconId,
|
||||||
icons: IconMap,
|
icons: IconMap,
|
||||||
canDownloadFavicon: !!this.model.url,
|
canDownloadFavicon: !!this.model.url,
|
||||||
customIcons: this.model.file.getCustomIcons()
|
customIcons,
|
||||||
|
hasCustomIcons
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +73,9 @@ class IconSelectView extends View {
|
||||||
.addClass('icon-select__icon--custom-selected')
|
.addClass('icon-select__icon--custom-selected')
|
||||||
.append(img);
|
.append(img);
|
||||||
this.downloadingFavicon = false;
|
this.downloadingFavicon = false;
|
||||||
|
|
||||||
|
const id = this.model.file.addCustomIcon(this.special.download.data);
|
||||||
|
this.emit('select', { id, custom: true });
|
||||||
};
|
};
|
||||||
img.onerror = (e) => {
|
img.onerror = (e) => {
|
||||||
logger.error('Favicon download error: ' + url, e);
|
logger.error('Favicon download error: ' + url, e);
|
||||||
|
|
|
@ -24,6 +24,7 @@ class ListSearchView extends View {
|
||||||
'click .list__search-btn-sort': 'sortOptionsClick',
|
'click .list__search-btn-sort': 'sortOptionsClick',
|
||||||
'click .list__search-icon-search': 'advancedSearchClick',
|
'click .list__search-icon-search': 'advancedSearchClick',
|
||||||
'click .list__search-btn-menu': 'toggleMenu',
|
'click .list__search-btn-menu': 'toggleMenu',
|
||||||
|
'click .list__search-icon-clear': 'clickClear',
|
||||||
'change .list__search-adv input[type=checkbox]': 'toggleAdvCheck'
|
'change .list__search-adv input[type=checkbox]': 'toggleAdvCheck'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -212,7 +213,9 @@ class ListSearchView extends View {
|
||||||
}
|
}
|
||||||
|
|
||||||
inputChange() {
|
inputChange() {
|
||||||
Events.emit('add-filter', { text: this.inputEl.val() });
|
const text = this.inputEl.val();
|
||||||
|
this.inputEl[0].parentElement.classList.toggle('list__search-field-wrap--text', text);
|
||||||
|
Events.emit('add-filter', { text });
|
||||||
}
|
}
|
||||||
|
|
||||||
inputFocus(e) {
|
inputFocus(e) {
|
||||||
|
@ -428,6 +431,11 @@ class ListSearchView extends View {
|
||||||
fileListUpdated() {
|
fileListUpdated() {
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clickClear() {
|
||||||
|
this.inputEl.val('');
|
||||||
|
this.inputChange();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ListSearchView };
|
export { ListSearchView };
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { View } from 'framework/views/view';
|
import { View } from 'framework/views/view';
|
||||||
|
import { Launcher } from 'comp/launcher';
|
||||||
import { Keys } from 'const/keys';
|
import { Keys } from 'const/keys';
|
||||||
import template from 'templates/modal.hbs';
|
import template from 'templates/modal.hbs';
|
||||||
|
|
||||||
|
@ -10,6 +11,7 @@ class ModalView extends View {
|
||||||
|
|
||||||
events = {
|
events = {
|
||||||
'click .modal__buttons button': 'buttonClick',
|
'click .modal__buttons button': 'buttonClick',
|
||||||
|
'click .modal__link': 'linkClick',
|
||||||
'click': 'bodyClick'
|
'click': 'bodyClick'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -55,6 +57,13 @@ class ModalView extends View {
|
||||||
this.closeWithResult(result);
|
this.closeWithResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
linkClick(e) {
|
||||||
|
if (Launcher) {
|
||||||
|
e.preventDefault();
|
||||||
|
Launcher.openLink(e.target.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bodyClick(e) {
|
bodyClick(e) {
|
||||||
if (typeof this.model.click === 'string' && !e.target.matches('button')) {
|
if (typeof this.model.click === 'string' && !e.target.matches('button')) {
|
||||||
this.closeWithResult(this.model.click);
|
this.closeWithResult(this.model.click);
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { StorageFileListView } from 'views/storage-file-list-view';
|
||||||
import { OpenChalRespView } from 'views/open-chal-resp-view';
|
import { OpenChalRespView } from 'views/open-chal-resp-view';
|
||||||
import { omit } from 'util/fn';
|
import { omit } from 'util/fn';
|
||||||
import { GeneratorView } from 'views/generator-view';
|
import { GeneratorView } from 'views/generator-view';
|
||||||
|
import { NativeModules } from 'comp/launcher/native-modules';
|
||||||
import template from 'templates/open.hbs';
|
import template from 'templates/open.hbs';
|
||||||
|
|
||||||
const logger = new Logger('open-view');
|
const logger = new Logger('open-view');
|
||||||
|
@ -57,12 +58,10 @@ class OpenView extends View {
|
||||||
};
|
};
|
||||||
|
|
||||||
params = null;
|
params = null;
|
||||||
|
|
||||||
passwordInput = null;
|
passwordInput = null;
|
||||||
|
|
||||||
busy = false;
|
busy = false;
|
||||||
|
|
||||||
currentSelectedIndex = -1;
|
currentSelectedIndex = -1;
|
||||||
|
encryptedPassword = null;
|
||||||
|
|
||||||
constructor(model) {
|
constructor(model) {
|
||||||
super(model);
|
super(model);
|
||||||
|
@ -152,6 +151,7 @@ class OpenView extends View {
|
||||||
|
|
||||||
windowFocused() {
|
windowFocused() {
|
||||||
this.inputEl.focus();
|
this.inputEl.focus();
|
||||||
|
this.checkIfEncryptedPasswordDateIsValid();
|
||||||
}
|
}
|
||||||
|
|
||||||
focusInput(focusOnMobile) {
|
focusInput(focusOnMobile) {
|
||||||
|
@ -237,8 +237,10 @@ class OpenView extends View {
|
||||||
if (!this.params.keyFileData) {
|
if (!this.params.keyFileData) {
|
||||||
this.params.keyFileName = null;
|
this.params.keyFileName = null;
|
||||||
}
|
}
|
||||||
|
this.encryptedPassword = null;
|
||||||
this.displayOpenFile();
|
this.displayOpenFile();
|
||||||
this.displayOpenKeyFile();
|
this.displayOpenKeyFile();
|
||||||
|
this.displayOpenDeviceOwnerAuth();
|
||||||
success = true;
|
success = true;
|
||||||
break;
|
break;
|
||||||
case 'xml':
|
case 'xml':
|
||||||
|
@ -248,7 +250,9 @@ class OpenView extends View {
|
||||||
this.params.path = null;
|
this.params.path = null;
|
||||||
this.params.storage = null;
|
this.params.storage = null;
|
||||||
this.params.rev = null;
|
this.params.rev = null;
|
||||||
|
this.encryptedPassword = null;
|
||||||
this.importDbWithXml();
|
this.importDbWithXml();
|
||||||
|
this.displayOpenDeviceOwnerAuth();
|
||||||
success = true;
|
success = true;
|
||||||
break;
|
break;
|
||||||
case 'kdb':
|
case 'kdb':
|
||||||
|
@ -341,6 +345,15 @@ class OpenView extends View {
|
||||||
.toggleClass('open__settings-yubikey--active', !!this.params.chalResp);
|
.toggleClass('open__settings-yubikey--active', !!this.params.chalResp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
displayOpenDeviceOwnerAuth() {
|
||||||
|
const available = !!this.encryptedPassword;
|
||||||
|
const passEmpty = !this.passwordInput.length;
|
||||||
|
const canUseEncryptedPassword = available && passEmpty;
|
||||||
|
this.el
|
||||||
|
.querySelector('.open__pass-enter-btn')
|
||||||
|
.classList.toggle('open__pass-enter-btn--touch-id', canUseEncryptedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
setFile(file, keyFile, fileReadyCallback) {
|
setFile(file, keyFile, fileReadyCallback) {
|
||||||
this.reading = 'fileData';
|
this.reading = 'fileData';
|
||||||
this.processFile(file, (success) => {
|
this.processFile(file, (success) => {
|
||||||
|
@ -479,6 +492,10 @@ class OpenView extends View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inputInput() {
|
||||||
|
this.displayOpenDeviceOwnerAuth();
|
||||||
|
}
|
||||||
|
|
||||||
toggleCapsLockWarning(on) {
|
toggleCapsLockWarning(on) {
|
||||||
this.$el.find('.open__pass-warning').toggleClass('invisible', !on);
|
this.$el.find('.open__pass-warning').toggleClass('invisible', !on);
|
||||||
}
|
}
|
||||||
|
@ -587,11 +604,12 @@ class OpenView extends View {
|
||||||
this.params.keyFileData = null;
|
this.params.keyFileData = null;
|
||||||
this.params.opts = fileInfo.opts;
|
this.params.opts = fileInfo.opts;
|
||||||
this.params.chalResp = fileInfo.chalResp;
|
this.params.chalResp = fileInfo.chalResp;
|
||||||
|
this.setEncryptedPassword(fileInfo);
|
||||||
|
|
||||||
this.displayOpenFile();
|
this.displayOpenFile();
|
||||||
this.displayOpenKeyFile();
|
this.displayOpenKeyFile();
|
||||||
this.displayOpenChalResp();
|
this.displayOpenChalResp();
|
||||||
|
this.displayOpenDeviceOwnerAuth();
|
||||||
this.openFileWithFingerprint(fileInfo);
|
|
||||||
|
|
||||||
if (fileWasClicked) {
|
if (fileWasClicked) {
|
||||||
this.focusInput(true);
|
this.focusInput(true);
|
||||||
|
@ -608,7 +626,9 @@ class OpenView extends View {
|
||||||
this.params.name = path.match(/[^/\\]*$/)[0];
|
this.params.name = path.match(/[^/\\]*$/)[0];
|
||||||
this.params.rev = null;
|
this.params.rev = null;
|
||||||
this.params.fileData = null;
|
this.params.fileData = null;
|
||||||
|
this.encryptedPassword = null;
|
||||||
this.displayOpenFile();
|
this.displayOpenFile();
|
||||||
|
this.displayOpenDeviceOwnerAuth();
|
||||||
if (keyFilePath) {
|
if (keyFilePath) {
|
||||||
const parsed = Launcher.parsePath(keyFilePath);
|
const parsed = Launcher.parsePath(keyFilePath);
|
||||||
this.params.keyFileName = parsed.file;
|
this.params.keyFileName = parsed.file;
|
||||||
|
@ -618,20 +638,6 @@ class OpenView extends View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openFileWithFingerprint(fileInfo) {
|
|
||||||
if (!fileInfo.fingerprint) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Launcher && Launcher.fingerprints) {
|
|
||||||
Launcher.fingerprints.auth(fileInfo.id, fileInfo.fingerprint, (password) => {
|
|
||||||
this.inputEl.val(password);
|
|
||||||
this.inputEl.trigger('input');
|
|
||||||
this.openDb();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createDemo() {
|
createDemo() {
|
||||||
if (!this.busy) {
|
if (!this.busy) {
|
||||||
this.closeConfig();
|
this.closeConfig();
|
||||||
|
@ -662,15 +668,37 @@ class OpenView extends View {
|
||||||
this.inputEl.attr('disabled', 'disabled');
|
this.inputEl.attr('disabled', 'disabled');
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
this.params.password = this.passwordInput.value;
|
this.params.password = this.passwordInput.value;
|
||||||
this.afterPaint(() => {
|
if (this.encryptedPassword && !this.params.password.length) {
|
||||||
this.model.openFile(this.params, (err) => this.openDbComplete(err));
|
logger.debug('Encrypting password using hardware decryption');
|
||||||
});
|
const touchIdPrompt = Locale.bioOpenAuthPrompt.replace('{}', this.params.name);
|
||||||
|
const encryptedPassword = kdbxweb.ProtectedValue.fromBase64(
|
||||||
|
this.encryptedPassword.value
|
||||||
|
);
|
||||||
|
NativeModules.hardwareDecrypt(encryptedPassword, touchIdPrompt)
|
||||||
|
.then((password) => {
|
||||||
|
this.params.password = password;
|
||||||
|
this.params.encryptedPassword = this.encryptedPassword;
|
||||||
|
this.model.openFile(this.params, (err) => this.openDbComplete(err));
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.message.includes('User refused')) {
|
||||||
|
err.userCanceled = true;
|
||||||
|
}
|
||||||
|
this.openDbComplete(err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.params.encryptedPassword = null;
|
||||||
|
this.afterPaint(() => {
|
||||||
|
this.model.openFile(this.params, (err) => this.openDbComplete(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openDbComplete(err) {
|
openDbComplete(err) {
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
this.$el.toggleClass('open--opening', false);
|
this.$el.toggleClass('open--opening', false);
|
||||||
this.inputEl.removeAttr('disabled').toggleClass('input--error', !!err);
|
const showInputError = err && !err.userCanceled;
|
||||||
|
this.inputEl.removeAttr('disabled').toggleClass('input--error', !!showInputError);
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.error('Error opening file', err);
|
logger.error('Error opening file', err);
|
||||||
this.focusInput(true);
|
this.focusInput(true);
|
||||||
|
@ -822,7 +850,9 @@ class OpenView extends View {
|
||||||
this.params.name = UrlFormat.getDataFileName(file.name);
|
this.params.name = UrlFormat.getDataFileName(file.name);
|
||||||
this.params.rev = file.rev;
|
this.params.rev = file.rev;
|
||||||
this.params.fileData = null;
|
this.params.fileData = null;
|
||||||
|
this.encryptedPassword = null;
|
||||||
this.displayOpenFile();
|
this.displayOpenFile();
|
||||||
|
this.displayOpenDeviceOwnerAuth();
|
||||||
}
|
}
|
||||||
|
|
||||||
showConfig(storage) {
|
showConfig(storage) {
|
||||||
|
@ -918,7 +948,9 @@ class OpenView extends View {
|
||||||
this.params.name = UrlFormat.getDataFileName(req.path);
|
this.params.name = UrlFormat.getDataFileName(req.path);
|
||||||
this.params.rev = stat.rev;
|
this.params.rev = stat.rev;
|
||||||
this.params.fileData = null;
|
this.params.fileData = null;
|
||||||
|
this.encryptedPassword = null;
|
||||||
this.displayOpenFile();
|
this.displayOpenFile();
|
||||||
|
this.displayOpenDeviceOwnerAuth();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1078,6 +1110,37 @@ class OpenView extends View {
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setEncryptedPassword(fileInfo) {
|
||||||
|
this.encryptedPassword = null;
|
||||||
|
if (!fileInfo.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (this.model.settings.deviceOwnerAuth) {
|
||||||
|
case 'memory':
|
||||||
|
this.encryptedPassword = this.model.getMemoryPassword(fileInfo.id);
|
||||||
|
break;
|
||||||
|
case 'file':
|
||||||
|
this.encryptedPassword = {
|
||||||
|
value: fileInfo.encryptedPassword,
|
||||||
|
date: fileInfo.encryptedPasswordDate
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.checkIfEncryptedPasswordDateIsValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIfEncryptedPasswordDateIsValid() {
|
||||||
|
if (this.encryptedPassword) {
|
||||||
|
const maxDate = new Date(this.encryptedPassword.date);
|
||||||
|
maxDate.setMinutes(
|
||||||
|
maxDate.getMinutes() + this.model.settings.deviceOwnerAuthTimeoutMinutes
|
||||||
|
);
|
||||||
|
if (maxDate < new Date()) {
|
||||||
|
this.encryptedPassword = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { OpenView };
|
export { OpenView };
|
||||||
|
|
|
@ -15,7 +15,8 @@ class SettingsAboutView extends View {
|
||||||
licenseLinkCCBY40: Links.LicenseLinkCCBY40,
|
licenseLinkCCBY40: Links.LicenseLinkCCBY40,
|
||||||
repoLink: Links.Repo,
|
repoLink: Links.Repo,
|
||||||
donationLink: Links.Donation,
|
donationLink: Links.Donation,
|
||||||
isDesktop: Features.isDesktop
|
isDesktop: Features.isDesktop,
|
||||||
|
year: new Date().getFullYear()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,8 @@ import { DateFormat } from 'comp/i18n/date-format';
|
||||||
import { Locale } from 'util/locale';
|
import { Locale } from 'util/locale';
|
||||||
import { SettingsLogsView } from 'views/settings/settings-logs-view';
|
import { SettingsLogsView } from 'views/settings/settings-logs-view';
|
||||||
import { SettingsPrvView } from 'views/settings/settings-prv-view';
|
import { SettingsPrvView } from 'views/settings/settings-prv-view';
|
||||||
import { mapObject } from 'util/fn';
|
import { mapObject, minmax } from 'util/fn';
|
||||||
|
import { ThemeWatcher } from 'comp/browser/theme-watcher';
|
||||||
import template from 'templates/settings/settings-general.hbs';
|
import template from 'templates/settings/settings-general.hbs';
|
||||||
|
|
||||||
class SettingsGeneralView extends View {
|
class SettingsGeneralView extends View {
|
||||||
|
@ -24,6 +25,7 @@ class SettingsGeneralView extends View {
|
||||||
|
|
||||||
events = {
|
events = {
|
||||||
'click .settings__general-theme': 'changeTheme',
|
'click .settings__general-theme': 'changeTheme',
|
||||||
|
'click .settings__general-auto-switch-theme': 'changeAuthSwitchTheme',
|
||||||
'change .settings__general-locale': 'changeLocale',
|
'change .settings__general-locale': 'changeLocale',
|
||||||
'change .settings__general-font-size': 'changeFontSize',
|
'change .settings__general-font-size': 'changeFontSize',
|
||||||
'change .settings__general-expand': 'changeExpandGroups',
|
'change .settings__general-expand': 'changeExpandGroups',
|
||||||
|
@ -34,6 +36,13 @@ class SettingsGeneralView extends View {
|
||||||
'change .settings__general-auto-save-interval': 'changeAutoSaveInterval',
|
'change .settings__general-auto-save-interval': 'changeAutoSaveInterval',
|
||||||
'change .settings__general-remember-key-files': 'changeRememberKeyFiles',
|
'change .settings__general-remember-key-files': 'changeRememberKeyFiles',
|
||||||
'change .settings__general-minimize': 'changeMinimize',
|
'change .settings__general-minimize': 'changeMinimize',
|
||||||
|
'change .settings__general-minimize-on-field-copy': 'changeMinimizeOnFieldCopy',
|
||||||
|
'change .settings__general-audit-passwords': 'changeAuditPasswords',
|
||||||
|
'change .settings__general-audit-password-entropy': 'changeAuditPasswordEntropy',
|
||||||
|
'change .settings__general-exclude-pins-from-audit': 'changeExcludePinsFromAudit',
|
||||||
|
'change .settings__general-check-passwords-on-hibp': 'changeCheckPasswordsOnHIBP',
|
||||||
|
'click .settings__general-toggle-help-hibp': 'clickToggleHelpHIBP',
|
||||||
|
'change .settings__general-audit-password-age': 'changeAuditPasswordAge',
|
||||||
'change .settings__general-lock-on-minimize': 'changeLockOnMinimize',
|
'change .settings__general-lock-on-minimize': 'changeLockOnMinimize',
|
||||||
'change .settings__general-lock-on-copy': 'changeLockOnCopy',
|
'change .settings__general-lock-on-copy': 'changeLockOnCopy',
|
||||||
'change .settings__general-lock-on-auto-type': 'changeLockOnAutoType',
|
'change .settings__general-lock-on-auto-type': 'changeLockOnAutoType',
|
||||||
|
@ -45,11 +54,15 @@ class SettingsGeneralView extends View {
|
||||||
'change .settings__general-direct-autotype': 'changeDirectAutotype',
|
'change .settings__general-direct-autotype': 'changeDirectAutotype',
|
||||||
'change .settings__general-field-label-dblclick-autotype':
|
'change .settings__general-field-label-dblclick-autotype':
|
||||||
'changeFieldLabelDblClickAutoType',
|
'changeFieldLabelDblClickAutoType',
|
||||||
|
'change .settings__general-use-legacy-autotype': 'changeUseLegacyAutoType',
|
||||||
|
'change .settings__general-device-owner-auth': 'changeDeviceOwnerAuth',
|
||||||
|
'change .settings__general-device-owner-auth-timeout': 'changeDeviceOwnerAuthTimeout',
|
||||||
'change .settings__general-titlebar-style': 'changeTitlebarStyle',
|
'change .settings__general-titlebar-style': 'changeTitlebarStyle',
|
||||||
'click .settings__general-update-btn': 'checkUpdate',
|
'click .settings__general-update-btn': 'checkUpdate',
|
||||||
'click .settings__general-restart-btn': 'restartApp',
|
'click .settings__general-restart-btn': 'installUpdateAndRestart',
|
||||||
'click .settings__general-download-update-btn': 'downloadUpdate',
|
'click .settings__general-download-update-btn': 'downloadUpdate',
|
||||||
'click .settings__general-update-found-btn': 'installFoundUpdate',
|
'click .settings__general-update-found-btn': 'installFoundUpdate',
|
||||||
|
'change .settings__general-disable-offline-storage': 'changeDisableOfflineStorage',
|
||||||
'change .settings__general-prv-check': 'changeStorageEnabled',
|
'change .settings__general-prv-check': 'changeStorageEnabled',
|
||||||
'click .settings__general-prv-logout': 'logoutFromStorage',
|
'click .settings__general-prv-logout': 'logoutFromStorage',
|
||||||
'click .settings__general-show-advanced': 'showAdvancedSettings',
|
'click .settings__general-show-advanced': 'showAdvancedSettings',
|
||||||
|
@ -61,8 +74,8 @@ class SettingsGeneralView extends View {
|
||||||
|
|
||||||
constructor(model, options) {
|
constructor(model, options) {
|
||||||
super(model, options);
|
super(model, options);
|
||||||
this.listenTo(UpdateModel, 'change:status', this.render);
|
this.listenTo(UpdateModel, 'change', this.render);
|
||||||
this.listenTo(UpdateModel, 'change:updateStatus', this.render);
|
this.listenTo(Events, 'theme-applied', this.render);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -72,7 +85,8 @@ class SettingsGeneralView extends View {
|
||||||
const storageProviders = this.getStorageProviders();
|
const storageProviders = this.getStorageProviders();
|
||||||
|
|
||||||
super.render({
|
super.render({
|
||||||
themes: mapObject(SettingsManager.allThemes, (theme) => Locale[theme]),
|
themes: this.getAllThemes(),
|
||||||
|
autoSwitchTheme: AppSettingsModel.autoSwitchTheme,
|
||||||
activeTheme: SettingsManager.activeTheme,
|
activeTheme: SettingsManager.activeTheme,
|
||||||
locales: SettingsManager.allLocales,
|
locales: SettingsManager.allLocales,
|
||||||
activeLocale: SettingsManager.activeLocale,
|
activeLocale: SettingsManager.activeLocale,
|
||||||
|
@ -86,6 +100,7 @@ class SettingsGeneralView extends View {
|
||||||
autoSaveInterval: AppSettingsModel.autoSaveInterval,
|
autoSaveInterval: AppSettingsModel.autoSaveInterval,
|
||||||
idleMinutes: AppSettingsModel.idleMinutes,
|
idleMinutes: AppSettingsModel.idleMinutes,
|
||||||
minimizeOnClose: AppSettingsModel.minimizeOnClose,
|
minimizeOnClose: AppSettingsModel.minimizeOnClose,
|
||||||
|
minimizeOnFieldCopy: AppSettingsModel.minimizeOnFieldCopy,
|
||||||
devTools: Launcher && Launcher.devTools,
|
devTools: Launcher && Launcher.devTools,
|
||||||
canAutoUpdate: Updater.enabled,
|
canAutoUpdate: Updater.enabled,
|
||||||
canAutoSaveOnClose: !!Launcher,
|
canAutoSaveOnClose: !!Launcher,
|
||||||
|
@ -93,6 +108,13 @@ class SettingsGeneralView extends View {
|
||||||
canDetectMinimize: !!Launcher,
|
canDetectMinimize: !!Launcher,
|
||||||
canDetectOsSleep: Launcher && Launcher.canDetectOsSleep(),
|
canDetectOsSleep: Launcher && Launcher.canDetectOsSleep(),
|
||||||
canAutoType: AutoType.enabled,
|
canAutoType: AutoType.enabled,
|
||||||
|
auditPasswords: AppSettingsModel.auditPasswords,
|
||||||
|
auditPasswordEntropy: AppSettingsModel.auditPasswordEntropy,
|
||||||
|
excludePinsFromAudit: AppSettingsModel.excludePinsFromAudit,
|
||||||
|
checkPasswordsOnHIBP: AppSettingsModel.checkPasswordsOnHIBP,
|
||||||
|
auditPasswordAge: AppSettingsModel.auditPasswordAge,
|
||||||
|
hibpLink: Links.HaveIBeenPwned,
|
||||||
|
hibpPrivacyLink: Links.HaveIBeenPwnedPrivacy,
|
||||||
lockOnMinimize: Launcher && AppSettingsModel.lockOnMinimize,
|
lockOnMinimize: Launcher && AppSettingsModel.lockOnMinimize,
|
||||||
lockOnCopy: AppSettingsModel.lockOnCopy,
|
lockOnCopy: AppSettingsModel.lockOnCopy,
|
||||||
lockOnAutoType: AppSettingsModel.lockOnAutoType,
|
lockOnAutoType: AppSettingsModel.lockOnAutoType,
|
||||||
|
@ -113,10 +135,16 @@ class SettingsGeneralView extends View {
|
||||||
useGroupIconForEntries: AppSettingsModel.useGroupIconForEntries,
|
useGroupIconForEntries: AppSettingsModel.useGroupIconForEntries,
|
||||||
directAutotype: AppSettingsModel.directAutotype,
|
directAutotype: AppSettingsModel.directAutotype,
|
||||||
fieldLabelDblClickAutoType: AppSettingsModel.fieldLabelDblClickAutoType,
|
fieldLabelDblClickAutoType: AppSettingsModel.fieldLabelDblClickAutoType,
|
||||||
supportsTitleBarStyles: Launcher && Features.supportsTitleBarStyles(),
|
useLegacyAutoType: AppSettingsModel.useLegacyAutoType,
|
||||||
|
supportsTitleBarStyles: Features.supportsTitleBarStyles(),
|
||||||
|
supportsCustomTitleBarAndDraggableWindow: Features.supportsCustomTitleBarAndDraggableWindow(),
|
||||||
titlebarStyle: AppSettingsModel.titlebarStyle,
|
titlebarStyle: AppSettingsModel.titlebarStyle,
|
||||||
storageProviders,
|
storageProviders,
|
||||||
showReloadApp: Features.isStandalone
|
showReloadApp: Features.isStandalone,
|
||||||
|
hasDeviceOwnerAuth: Features.isDesktop && Features.isMac,
|
||||||
|
deviceOwnerAuth: AppSettingsModel.deviceOwnerAuth,
|
||||||
|
deviceOwnerAuthTimeout: AppSettingsModel.deviceOwnerAuthTimeoutMinutes,
|
||||||
|
disableOfflineStorage: AppSettingsModel.disableOfflineStorage
|
||||||
});
|
});
|
||||||
this.renderProviderViews(storageProviders);
|
this.renderProviderViews(storageProviders);
|
||||||
}
|
}
|
||||||
|
@ -204,16 +232,49 @@ class SettingsGeneralView extends View {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllThemes() {
|
||||||
|
const { autoSwitchTheme } = AppSettingsModel;
|
||||||
|
if (autoSwitchTheme) {
|
||||||
|
const themes = {};
|
||||||
|
const ignoredThemes = {};
|
||||||
|
for (const config of SettingsManager.autoSwitchedThemes) {
|
||||||
|
ignoredThemes[config.dark] = true;
|
||||||
|
ignoredThemes[config.light] = true;
|
||||||
|
const activeTheme = ThemeWatcher.dark ? config.dark : config.light;
|
||||||
|
themes[activeTheme] = Locale[config.name];
|
||||||
|
}
|
||||||
|
for (const [th, name] of Object.entries(SettingsManager.allThemes)) {
|
||||||
|
if (!ignoredThemes[th]) {
|
||||||
|
themes[th] = Locale[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return themes;
|
||||||
|
} else {
|
||||||
|
return mapObject(SettingsManager.allThemes, (theme) => Locale[theme]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
changeTheme(e) {
|
changeTheme(e) {
|
||||||
const theme = e.target.closest('.settings__general-theme').dataset.theme;
|
const theme = e.target.closest('.settings__general-theme').dataset.theme;
|
||||||
if (theme === '...') {
|
if (theme === '...') {
|
||||||
this.goToPlugins();
|
this.goToPlugins();
|
||||||
} else {
|
} else {
|
||||||
AppSettingsModel.theme = theme;
|
const changedInSettings = AppSettingsModel.theme !== theme;
|
||||||
this.render();
|
if (changedInSettings) {
|
||||||
|
AppSettingsModel.theme = theme;
|
||||||
|
} else {
|
||||||
|
SettingsManager.setTheme(theme);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeAuthSwitchTheme(e) {
|
||||||
|
const autoSwitchTheme = e.target.checked;
|
||||||
|
AppSettingsModel.autoSwitchTheme = autoSwitchTheme;
|
||||||
|
SettingsManager.darkModeChanged();
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
changeLocale(e) {
|
changeLocale(e) {
|
||||||
const locale = e.target.value;
|
const locale = e.target.value;
|
||||||
if (locale === '...') {
|
if (locale === '...') {
|
||||||
|
@ -283,6 +344,43 @@ class SettingsGeneralView extends View {
|
||||||
AppSettingsModel.minimizeOnClose = minimizeOnClose;
|
AppSettingsModel.minimizeOnClose = minimizeOnClose;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeMinimizeOnFieldCopy(e) {
|
||||||
|
const minimizeOnFieldCopy = e.target.checked || false;
|
||||||
|
AppSettingsModel.minimizeOnFieldCopy = minimizeOnFieldCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeAuditPasswords(e) {
|
||||||
|
const auditPasswords = e.target.checked || false;
|
||||||
|
AppSettingsModel.auditPasswords = auditPasswords;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeAuditPasswordEntropy(e) {
|
||||||
|
const auditPasswordEntropy = e.target.checked || false;
|
||||||
|
AppSettingsModel.auditPasswordEntropy = auditPasswordEntropy;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeExcludePinsFromAudit(e) {
|
||||||
|
const excludePinsFromAudit = e.target.checked || false;
|
||||||
|
AppSettingsModel.excludePinsFromAudit = excludePinsFromAudit;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeCheckPasswordsOnHIBP(e) {
|
||||||
|
if (e.target.closest('a')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const checkPasswordsOnHIBP = e.target.checked || false;
|
||||||
|
AppSettingsModel.checkPasswordsOnHIBP = checkPasswordsOnHIBP;
|
||||||
|
}
|
||||||
|
|
||||||
|
clickToggleHelpHIBP() {
|
||||||
|
this.el.querySelector('.settings__general-help-hibp').classList.toggle('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
changeAuditPasswordAge(e) {
|
||||||
|
const auditPasswordAge = e.target.value | 0;
|
||||||
|
AppSettingsModel.auditPasswordAge = auditPasswordAge;
|
||||||
|
}
|
||||||
|
|
||||||
changeLockOnMinimize(e) {
|
changeLockOnMinimize(e) {
|
||||||
const lockOnMinimize = e.target.checked || false;
|
const lockOnMinimize = e.target.checked || false;
|
||||||
AppSettingsModel.lockOnMinimize = lockOnMinimize;
|
AppSettingsModel.lockOnMinimize = lockOnMinimize;
|
||||||
|
@ -324,13 +422,11 @@ class SettingsGeneralView extends View {
|
||||||
changeUseGroupIconForEntries(e) {
|
changeUseGroupIconForEntries(e) {
|
||||||
const useGroupIconForEntries = e.target.checked || false;
|
const useGroupIconForEntries = e.target.checked || false;
|
||||||
AppSettingsModel.useGroupIconForEntries = useGroupIconForEntries;
|
AppSettingsModel.useGroupIconForEntries = useGroupIconForEntries;
|
||||||
Events.emit('refresh');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeDirectAutotype(e) {
|
changeDirectAutotype(e) {
|
||||||
const directAutotype = e.target.checked || false;
|
const directAutotype = e.target.checked || false;
|
||||||
AppSettingsModel.directAutotype = directAutotype;
|
AppSettingsModel.directAutotype = directAutotype;
|
||||||
Events.emit('refresh');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeFieldLabelDblClickAutoType(e) {
|
changeFieldLabelDblClickAutoType(e) {
|
||||||
|
@ -339,9 +435,36 @@ class SettingsGeneralView extends View {
|
||||||
Events.emit('refresh');
|
Events.emit('refresh');
|
||||||
}
|
}
|
||||||
|
|
||||||
restartApp() {
|
changeUseLegacyAutoType(e) {
|
||||||
|
const useLegacyAutoType = e.target.checked || false;
|
||||||
|
AppSettingsModel.useLegacyAutoType = useLegacyAutoType;
|
||||||
|
Events.emit('refresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
changeDeviceOwnerAuth(e) {
|
||||||
|
const deviceOwnerAuth = e.target.value || null;
|
||||||
|
|
||||||
|
let deviceOwnerAuthTimeoutMinutes = AppSettingsModel.deviceOwnerAuthTimeoutMinutes | 0;
|
||||||
|
if (deviceOwnerAuth) {
|
||||||
|
const timeouts = { memory: [30, 10080], file: [30, 525600] };
|
||||||
|
const [tMin, tMax] = timeouts[deviceOwnerAuth] || [0, 0];
|
||||||
|
deviceOwnerAuthTimeoutMinutes = minmax(deviceOwnerAuthTimeoutMinutes, tMin, tMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppSettingsModel.set({ deviceOwnerAuth, deviceOwnerAuthTimeoutMinutes });
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
this.appModel.checkEncryptedPasswordsStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
changeDeviceOwnerAuthTimeout(e) {
|
||||||
|
const deviceOwnerAuthTimeout = e.target.value | 0;
|
||||||
|
AppSettingsModel.deviceOwnerAuthTimeoutMinutes = deviceOwnerAuthTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
installUpdateAndRestart() {
|
||||||
if (Launcher) {
|
if (Launcher) {
|
||||||
Launcher.requestRestart();
|
Updater.installAndRestart();
|
||||||
} else {
|
} else {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
@ -353,7 +476,7 @@ class SettingsGeneralView extends View {
|
||||||
|
|
||||||
installFoundUpdate() {
|
installFoundUpdate() {
|
||||||
Updater.update(true, () => {
|
Updater.update(true, () => {
|
||||||
Launcher.requestRestart();
|
Updater.installAndRestart();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,6 +486,14 @@ class SettingsGeneralView extends View {
|
||||||
Events.emit('refresh');
|
Events.emit('refresh');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeDisableOfflineStorage(e) {
|
||||||
|
const disableOfflineStorage = e.target.checked;
|
||||||
|
AppSettingsModel.disableOfflineStorage = disableOfflineStorage;
|
||||||
|
if (disableOfflineStorage) {
|
||||||
|
this.appModel.deleteAllCachedFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
changeStorageEnabled(e) {
|
changeStorageEnabled(e) {
|
||||||
const storage = Storage[$(e.target).data('storage')];
|
const storage = Storage[$(e.target).data('storage')];
|
||||||
if (storage) {
|
if (storage) {
|
||||||
|
|
|
@ -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 & {
|
.titlebar-hidden-inset & {
|
||||||
padding-top: $titlebar-padding-large;
|
padding-top: $titlebar-padding-large;
|
||||||
}
|
}
|
||||||
|
.titlebar-custom.titlebar-hidden &,
|
||||||
|
.titlebar-custom.titlebar-hidden-inset & {
|
||||||
|
padding-top: $titlebar-custom-height;
|
||||||
|
}
|
||||||
.fullscreen .app & {
|
.fullscreen .app & {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
@ -123,6 +127,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@include padding-if-titlebar;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__panel {
|
&__panel {
|
||||||
|
|
|
@ -575,6 +575,50 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__issues {
|
||||||
|
margin-top: $base-padding-v;
|
||||||
|
color: var(--text-contrast-error-color);
|
||||||
|
background-color: var(--error-color);
|
||||||
|
border-radius: var(--block-border-radius);
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
&-body {
|
||||||
|
padding: $medium-padding-v 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
> a {
|
||||||
|
color: var(--text-contrast-error-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-icon {
|
||||||
|
padding: $medium-padding;
|
||||||
|
width: 1em;
|
||||||
|
&-spin {
|
||||||
|
display: none;
|
||||||
|
.details__issues-icon--loading & {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-warning {
|
||||||
|
.details__issues-icon--loading & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-close-btn {
|
||||||
|
padding: $medium-padding;
|
||||||
|
cursor: pointer;
|
||||||
|
align-self: flex-start;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity $base-duration $base-timing;
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__buttons {
|
&__buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
user-select: none;
|
user-select: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@include padding-if-titlebar;
|
||||||
|
|
||||||
> .scroller {
|
> .scroller {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
user-select: none;
|
user-select: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@include padding-if-titlebar;
|
||||||
|
|
||||||
> .scroller {
|
> .scroller {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
@include padding-if-titlebar;
|
||||||
|
|
||||||
&__body {
|
&__body {
|
||||||
@include scrollbar-on-hover;
|
@include scrollbar-on-hover;
|
||||||
|
|
|
@ -55,21 +55,30 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: 0.6em !important;
|
border-radius: 0.6em !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
padding-left: 0.4em;
|
|
||||||
background-color: var(--secondary-background-color) !important;
|
background-color: var(--secondary-background-color) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&-icon-search {
|
&-icon-search,
|
||||||
|
&-icon-clear {
|
||||||
color: var(--muted-color);
|
color: var(--muted-color);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.5em;
|
top: 0.53em;
|
||||||
right: 0.5em;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--medium-color);
|
color: var(--medium-color);
|
||||||
}
|
}
|
||||||
@include mobile {
|
@include mobile {
|
||||||
top: 0.5em;
|
top: 0.6em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-icon-search {
|
||||||
|
left: 0.6em;
|
||||||
|
}
|
||||||
|
&-icon-clear {
|
||||||
|
right: 0.6em;
|
||||||
|
display: none;
|
||||||
|
.list__search-field-wrap--text & {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&-btn-new,
|
&-btn-new,
|
||||||
|
@ -98,6 +107,7 @@
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
padding: 0 $small-spacing;
|
||||||
&-text {
|
&-text {
|
||||||
flex: 100%;
|
flex: 100%;
|
||||||
padding: $base-padding-v 0;
|
padding: $base-padding-v 0;
|
||||||
|
|
|
@ -108,6 +108,18 @@
|
||||||
.open--opening & {
|
.open--opening & {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
&-icon-enter {
|
||||||
|
display: block;
|
||||||
|
.open__pass-enter-btn--touch-id & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-icon-touch-id {
|
||||||
|
display: none;
|
||||||
|
.open__pass-enter-btn--touch-id & {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&-opening-icon {
|
&-opening-icon {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
@include scrollbar-on-hover;
|
@include scrollbar-on-hover;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@include padding-if-titlebar;
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
margin: $medium-padding;
|
margin: $medium-padding;
|
||||||
|
@ -171,10 +172,10 @@
|
||||||
}
|
}
|
||||||
&__general-update-btn {
|
&__general-update-btn {
|
||||||
width: 15em;
|
width: 15em;
|
||||||
margin-right: $small-spacing;
|
|
||||||
}
|
}
|
||||||
&__general-storage-header {
|
&__general-storage-header {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
line-height: 1.3em;
|
||||||
}
|
}
|
||||||
&__general-prv {
|
&__general-prv {
|
||||||
margin-bottom: $base-padding-v;
|
margin-bottom: $base-padding-v;
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
padding: $medium-padding;
|
padding: $medium-padding;
|
||||||
|
@include padding-if-titlebar;
|
||||||
|
|
||||||
&__space {
|
&__space {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
@ -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 {
|
&.input-search {
|
||||||
padding-right: 1.7em;
|
padding-left: 2.9em;
|
||||||
|
padding-right: 1.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
|
|
|
@ -58,6 +58,7 @@ $fa-var-unlock: next-fa-glyph();
|
||||||
$fa-var-lock: next-fa-glyph();
|
$fa-var-lock: next-fa-glyph();
|
||||||
$fa-var-check: next-fa-glyph();
|
$fa-var-check: next-fa-glyph();
|
||||||
$fa-var-times: next-fa-glyph();
|
$fa-var-times: next-fa-glyph();
|
||||||
|
$fa-var-times-circle: next-fa-glyph();
|
||||||
$fa-var-folder: next-fa-glyph();
|
$fa-var-folder: next-fa-glyph();
|
||||||
$fa-var-folder-open: next-fa-glyph();
|
$fa-var-folder-open: next-fa-glyph();
|
||||||
$fa-var-ban: next-fa-glyph();
|
$fa-var-ban: next-fa-glyph();
|
||||||
|
@ -123,6 +124,7 @@ $fa-var-inbox: next-fa-glyph();
|
||||||
$fa-var-save: next-fa-glyph();
|
$fa-var-save: next-fa-glyph();
|
||||||
$fa-var-hdd: next-fa-glyph();
|
$fa-var-hdd: next-fa-glyph();
|
||||||
$fa-var-dot-circle: next-fa-glyph();
|
$fa-var-dot-circle: next-fa-glyph();
|
||||||
|
$fa-var-user: next-fa-glyph();
|
||||||
$fa-var-user-lock: next-fa-glyph();
|
$fa-var-user-lock: next-fa-glyph();
|
||||||
$fa-var-terminal: next-fa-glyph();
|
$fa-var-terminal: next-fa-glyph();
|
||||||
$fa-var-print: next-fa-glyph();
|
$fa-var-print: next-fa-glyph();
|
||||||
|
@ -193,3 +195,8 @@ $fa-var-paint-brush: next-fa-glyph();
|
||||||
$fa-var-at: next-fa-glyph();
|
$fa-var-at: next-fa-glyph();
|
||||||
$fa-var-usb-token: next-fa-glyph();
|
$fa-var-usb-token: next-fa-glyph();
|
||||||
$fa-var-bell: next-fa-glyph();
|
$fa-var-bell: next-fa-glyph();
|
||||||
|
$fa-var-fingerprint: next-fa-glyph();
|
||||||
|
$fa-var-titlebar-close: next-fa-glyph();
|
||||||
|
$fa-var-titlebar-maximize: next-fa-glyph();
|
||||||
|
$fa-var-titlebar-minimize: next-fa-glyph();
|
||||||
|
$fa-var-titlebar-restore: next-fa-glyph();
|
||||||
|
|
|
@ -69,7 +69,8 @@
|
||||||
selectable-on-secondary-item-color:
|
selectable-on-secondary-item-color:
|
||||||
mix(map-get($t, medium-color), map-get($t, background-color), 14%),
|
mix(map-get($t, medium-color), map-get($t, background-color), 14%),
|
||||||
clickable-on-secondary-color:
|
clickable-on-secondary-color:
|
||||||
mix(map-get($t, medium-color), map-get($t, background-color), 75%)
|
mix(map-get($t, medium-color), map-get($t, background-color), 75%),
|
||||||
|
titlebar-button-background-color: rgba(map-get($t, text-color), 0.085)
|
||||||
),
|
),
|
||||||
$t
|
$t
|
||||||
);
|
);
|
||||||
|
|
|
@ -66,9 +66,12 @@ $titlebar-padding-large: 40px;
|
||||||
|
|
||||||
// Animations
|
// Animations
|
||||||
$base-duration: 150ms;
|
$base-duration: 150ms;
|
||||||
|
$fast-duration: 80ms;
|
||||||
$base-timing: ease;
|
$base-timing: ease;
|
||||||
$slow-transition-in: $base-duration * 2 ease-in;
|
$slow-transition-in: $base-duration * 2 ease-in;
|
||||||
$slow-transition-out: $base-duration ease-out;
|
$slow-transition-out: $base-duration ease-out;
|
||||||
|
$fast-transition-in: $fast-duration ease-in;
|
||||||
|
$fast-transition-out: $fast-duration ease-out;
|
||||||
$tip-transition-in: 500ms $ease-in-expo;
|
$tip-transition-in: 500ms $ease-in-expo;
|
||||||
$tip-transition-out: $slow-transition-out;
|
$tip-transition-out: $slow-transition-out;
|
||||||
|
|
||||||
|
@ -81,3 +84,7 @@ $z-index-modal: 100000;
|
||||||
// Screen sizes
|
// Screen sizes
|
||||||
$tablet-width: 736px;
|
$tablet-width: 736px;
|
||||||
$mobile-width: 620px;
|
$mobile-width: 620px;
|
||||||
|
|
||||||
|
// Title bar and window buttons
|
||||||
|
$titlebar-custom-height: 32px;
|
||||||
|
$titlebar-close-button-background-color: #d71525;
|
||||||
|
|
|
@ -25,6 +25,15 @@
|
||||||
animation: shake 50s cubic-bezier(0.36, 0.07, 0.19, 0.97) 0s;
|
animation: shake 50s cubic-bezier(0.36, 0.07, 0.19, 0.97) 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fade-in $fast-transition-in 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-out {
|
||||||
|
opacity: 0;
|
||||||
|
animation: fade-out $fast-transition-out 0s;
|
||||||
|
}
|
||||||
|
|
||||||
.rotate-90,
|
.rotate-90,
|
||||||
.fa.rotate-90:before {
|
.fa.rotate-90:before {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
|
@ -52,6 +61,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes shake {
|
@keyframes shake {
|
||||||
0%,
|
0%,
|
||||||
1%,
|
1%,
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
padding-bottom: $base-padding-h;
|
padding-bottom: $base-padding-h;
|
||||||
&--custom {
|
&--custom,
|
||||||
|
&--actions {
|
||||||
padding-top: $base-padding-h;
|
padding-top: $base-padding-h;
|
||||||
border-top: 1px solid var(--light-border-color);
|
border-top: 1px solid var(--light-border-color);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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/modal';
|
||||||
@import 'common/scroll';
|
@import 'common/scroll';
|
||||||
@import 'common/tip';
|
@import 'common/tip';
|
||||||
|
@import 'common/titlebar';
|
||||||
|
|
||||||
@import 'areas/app';
|
@import 'areas/app';
|
||||||
@import 'areas/auto-type';
|
@import 'areas/auto-type';
|
||||||
|
@ -38,3 +39,4 @@ $fa-font-path: '~font-awesome/fonts';
|
||||||
@import 'areas/open';
|
@import 'areas/open';
|
||||||
@import 'areas/settings';
|
@import 'areas/settings';
|
||||||
@import 'areas/import-csv';
|
@import 'areas/import-csv';
|
||||||
|
@import 'areas/titlebar';
|
||||||
|
|
|
@ -4,9 +4,13 @@ $themes: ();
|
||||||
@import 'dark';
|
@import 'dark';
|
||||||
@import 'light';
|
@import 'light';
|
||||||
@import 'dark-brown';
|
@import 'dark-brown';
|
||||||
|
@import 'light-brown';
|
||||||
@import 'flat-blue';
|
@import 'flat-blue';
|
||||||
|
@import 'light-blue';
|
||||||
@import 'terminal';
|
@import 'terminal';
|
||||||
|
@import 'light-terminal';
|
||||||
@import 'high-contrast';
|
@import 'high-contrast';
|
||||||
|
@import 'dark-contrast';
|
||||||
@import 'solarized-dark';
|
@import 'solarized-dark';
|
||||||
@import 'solarized-light';
|
@import 'solarized-light';
|
||||||
|
|
||||||
|
|
|
@ -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-item-color: #2366d9;
|
||||||
--selected-on-secondary-item-color: #d6d6d6;
|
--selected-on-secondary-item-color: #d6d6d6;
|
||||||
--selected-item-text-color: #f6f6f6;
|
--selected-item-text-color: #f6f6f6;
|
||||||
--open-icon-color: var(--muted-color);
|
--open-icon-color: #565656;
|
||||||
|
|
||||||
.list__item--active .blue-color {
|
.list__item--active .blue-color {
|
||||||
color: #7baeff;
|
color: #7baeff;
|
||||||
|
|
|
@ -6,6 +6,10 @@
|
||||||
padding: $base-padding;
|
padding: $base-padding;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
&__body {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
&__block {
|
&__block {
|
||||||
margin-bottom: $base-padding-v;
|
margin-bottom: $base-padding-v;
|
||||||
> a,
|
> a,
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
.info-btn {
|
.info-btn {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--muted-color);
|
color: var(--muted-color);
|
||||||
|
margin-left: $tiny-spacing;
|
||||||
|
position: relative;
|
||||||
|
top: 0.15em;
|
||||||
|
font-size: 1.1em;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<div class="app">
|
<div class="app">
|
||||||
{{#if beta}}<div class="app__beta"><i class="fa fa-exclamation-triangle"></i> {{res 'appBeta'}}</div>{{/if}}
|
{{#if beta}}<div class="app__beta"><i class="fa fa-exclamation-triangle"></i> {{res 'appBeta'}}</div>{{/if}}
|
||||||
{{#ifeq titlebarStyle 'hidden'}}<div class="app__titlebar-drag"></div>{{/ifeq}}
|
{{#if customTitlebar}}
|
||||||
|
<div class="app__titlebar"></div>
|
||||||
|
{{else}}
|
||||||
|
{{#ifeq titlebarStyle 'hidden'}}<div class="app__titlebar-drag"></div>{{/ifeq}}
|
||||||
|
{{/if}}
|
||||||
<div class="app__body">
|
<div class="app__body">
|
||||||
<div class="app__menu"></div>
|
<div class="app__menu"></div>
|
||||||
<div class="app__menu-drag"></div>
|
<div class="app__menu-drag"></div>
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
<div class="auto-type-hint">
|
<div class="auto-type-hint">
|
||||||
<a href="{{link}}" class="auto-type-hint__link-details" target="_blank">{{res 'autoTypeLink'}}</a>
|
<div class="auto-type-hint__body">
|
||||||
<div class="auto-type-hint__block">
|
<a href="{{link}}" class="auto-type-hint__link-details" target="_blank">{{res 'autoTypeLink'}}</a>
|
||||||
<div>{{res 'autoTypeEntryFields'}}:</div>
|
<div class="auto-type-hint__block">
|
||||||
<a>{TITLE}</a><a>{USERNAME}</a><a>{URL}</a><a>{PASSWORD}</a><a>{NOTES}</a><a>{GROUP}</a>
|
<div>{{res 'autoTypeEntryFields'}}:</div>
|
||||||
<a>{TOTP}</a><a>{S:Custom Field Name}</a>
|
<a>{TITLE}</a><a>{USERNAME}</a><a>{URL}</a><a>{PASSWORD}</a><a>{NOTES}</a><a>{GROUP}</a>
|
||||||
</div>
|
<a>{TOTP}</a><a>{S:Custom Field Name}</a>
|
||||||
<div class="auto-type-hint__block">
|
</div>
|
||||||
<div>{{res 'autoTypeModifiers'}}:</div>
|
<div class="auto-type-hint__block">
|
||||||
<a>+ (shift)</a><a>% (alt)</a><a>^ ({{cmd}})</a>{{#if hasCtrl}}<a>^^ (ctrl)</a>{{/if}}
|
<div>{{res 'autoTypeModifiers'}}:</div>
|
||||||
</div>
|
<a>+ (shift)</a><a>% (alt)</a><a>^ ({{cmd}})</a>{{#if hasCtrl}}<a>^^ (ctrl)</a>{{/if}}
|
||||||
<div class="auto-type-hint__block">
|
</div>
|
||||||
<div>{{res 'autoTypeKeys'}}:</div>
|
<div class="auto-type-hint__block">
|
||||||
<a>{TAB}</a><a>{ENTER}</a><a>{SPACE}</a><a>{UP}</a><a>{DOWN}</a><a>{LEFT}</a><a>{RIGHT}</a><a>{HOME}</a><a>{END}</a>
|
<div>{{res 'autoTypeKeys'}}:</div>
|
||||||
<a>{+}</a><a>{%}</a><a>{^}</a><a>{~}</a><a>{(}</a><a>{)}</a><a>{[}</a><a>{]}</a><a>\{{}</a><a>{}}</a>
|
<a>{TAB}</a><a>{ENTER}</a><a>{SPACE}</a><a>{UP}</a><a>{DOWN}</a><a>{LEFT}</a><a>{RIGHT}</a><a>{HOME}</a><a>{END}</a>
|
||||||
|
<a>{+}</a><a>{%}</a><a>{^}</a><a>{~}</a><a>{(}</a><a>{)}</a><a>{[}</a><a>{]}</a><a>\{{}</a><a>{}}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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 class="scroller__bar-wrapper"><div class="scroller__bar"></div></div>
|
||||||
</div>
|
</div>
|
||||||
{{#unless readOnly}}
|
{{#unless readOnly}}
|
||||||
|
<div class="details__issues-container">
|
||||||
|
</div>
|
||||||
<div class="details__buttons">
|
<div class="details__buttons">
|
||||||
{{#if deleted~}}
|
{{#if deleted~}}
|
||||||
<i class="details__buttons-trash-del fa fa-minus-circle" title="{{res 'detDelEntryPerm'}}" tip-placement="top"></i>
|
<i class="details__buttons-trash-del fa fa-minus-circle" title="{{res 'detDelEntryPerm'}}" tip-placement="top"></i>
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
<i class="fa fa-{{icon}} icon-select__icon {{#ifeq ix ../sel}}icon-select__icon--active{{/ifeq}}" data-val="{{ix}}"></i>
|
<i class="fa fa-{{icon}} icon-select__icon {{#ifeq ix ../sel}}icon-select__icon--active{{/ifeq}}" data-val="{{ix}}"></i>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
<div class="icon-select__items icon-select__items--custom">
|
<div class="icon-select__items icon-select__items--actions">
|
||||||
<input type="file" class="icon-select__file-input hide-by-pos" accept="image/*" />
|
<input type="file" class="icon-select__file-input hide-by-pos" accept="image/*" />
|
||||||
{{#if canDownloadFavicon}}
|
{{#if canDownloadFavicon}}
|
||||||
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-download"
|
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-download"
|
||||||
data-val="special" data-special="download" title="{{res 'iconFavTitle'}}">
|
data-val="special" data-special="download" title="{{res 'iconFavTitle'}}">
|
||||||
<i class="fa fa-cloud-download-alt"></i>
|
<i class="fa fa-cloud-download-alt"></i>
|
||||||
</span>
|
</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -16,6 +16,9 @@
|
||||||
data-val="special" data-special="select" title="{{res 'iconSelCustom'}}">
|
data-val="special" data-special="select" title="{{res 'iconSelCustom'}}">
|
||||||
<i class="fa fa-ellipsis-h"></i>
|
<i class="fa fa-ellipsis-h"></i>
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
{{#if hasCustomIcons}}
|
||||||
|
<div class="icon-select__items icon-select__items--custom">
|
||||||
{{#each customIcons as |icon ci|}}
|
{{#each customIcons as |icon ci|}}
|
||||||
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-custom {{#ifeq ci ../sel}}icon-select__icon--active{{/ifeq}}"
|
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-custom {{#ifeq ci ../sel}}icon-select__icon--active{{/ifeq}}"
|
||||||
data-val="{{ci}}">
|
data-val="{{ci}}">
|
||||||
|
@ -23,4 +26,5 @@
|
||||||
</span>
|
</span>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,11 +4,14 @@
|
||||||
<i class="fa fa-bars"></i>
|
<i class="fa fa-bars"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="list__search-field-wrap">
|
<div class="list__search-field-wrap">
|
||||||
<input type="text" class="list__search-field input-padding-right" autocomplete="off" spellcheck="false">
|
<input type="text" class="list__search-field input-search" autocomplete="off" spellcheck="false">
|
||||||
<div class="list__search-icon-search" title="{{res 'searchAdvTitle'}}">
|
<div class="list__search-icon-search" title="{{res 'searchAdvTitle'}}">
|
||||||
<i class="fa fa-search"></i>
|
<i class="fa fa-search"></i>
|
||||||
<i class="fa fa-caret-down"></i>
|
<i class="fa fa-caret-down"></i>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="list__search-icon-clear">
|
||||||
|
<i class="fa fa-times-circle"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list__search-btn-new {{#unless canCreate}}hide{{/unless}}" title="{{res 'searchAddNew'}}">
|
<div class="list__search-btn-new {{#unless canCreate}}hide{{/unless}}" title="{{res 'searchAddNew'}}">
|
||||||
<i class="fa fa-plus"></i>
|
<i class="fa fa-plus"></i>
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
{{#unless @last}}<br/>{{/unless}}
|
{{#unless @last}}<br/>{{/unless}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{#if pre}}<pre class="modal__pre">{{pre}}</pre>{{/if}}
|
{{#if pre}}<pre class="modal__pre">{{pre}}</pre>{{/if}}
|
||||||
|
{{#if link}}<a href="{{link}}" class="modal__link" target="_blank">{{link}}</a>{{/if}}
|
||||||
{{#if hint}}<p class="muted-color">{{hint}}</p>{{/if}}
|
{{#if hint}}<p class="muted-color">{{hint}}</p>{{/if}}
|
||||||
{{#if checkbox}}
|
{{#if checkbox}}
|
||||||
<div class="modal__check-wrap"><input type="checkbox" id="modal__check" /><label for="modal__check">{{checkbox}}</label></div>
|
<div class="modal__check-wrap"><input type="checkbox" id="modal__check" /><label for="modal__check">{{checkbox}}</label></div>
|
||||||
|
|
|
@ -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">
|
<div class="open__pass-field-wrap">
|
||||||
<input class="open__pass-input" name="password" type="password" size="30" autocomplete="new-password" maxlength="1024"
|
<input class="open__pass-input" name="password" type="password" size="30" autocomplete="new-password" maxlength="1024"
|
||||||
placeholder="{{#if canOpen}}{{res 'openClickToOpen'}}{{/if}}" readonly tabindex="23" />
|
placeholder="{{#if canOpen}}{{res 'openClickToOpen'}}{{/if}}" readonly tabindex="23" />
|
||||||
<div class="open__pass-enter-btn" tabindex="24"><i class="fa fa-level-down-alt rotate-90"></i></div>
|
<div class="open__pass-enter-btn" tabindex="24">
|
||||||
|
<i class="fa fa-level-down-alt rotate-90 open__pass-enter-btn-icon-enter"></i>
|
||||||
|
<i class="fa fa-fingerprint open__pass-enter-btn-icon-touch-id"></i>
|
||||||
|
</div>
|
||||||
<div class="open__pass-opening-icon"><i class="fa fa-spinner spin"></i></div>
|
<div class="open__pass-opening-icon"><i class="fa fa-spinner spin"></i></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="open__settings">
|
<div class="open__settings">
|
||||||
|
|
|
@ -37,11 +37,12 @@
|
||||||
{{#if isDesktop}}
|
{{#if isDesktop}}
|
||||||
<h3>Desktop modules</h3>
|
<h3>Desktop modules</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="https://github.com/antelle/node-stream-zip" target="_blank">node-stream-zip</a><span class="muted-color">, node.js library for fast reading of large ZIPs, © 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/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/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/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-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>
|
</ul>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
@ -67,7 +68,7 @@
|
||||||
|
|
||||||
<h2>{{res 'setAboutLic'}}</h2>
|
<h2>{{res 'setAboutLic'}}</h2>
|
||||||
<p>{{res 'setAboutLicComment'}}:</p>
|
<p>{{res 'setAboutLicComment'}}:</p>
|
||||||
<p>Copyright © 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
|
<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
||||||
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
||||||
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
|
|
@ -66,6 +66,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" class="settings__input input-base settings__general-auto-switch-theme" id="settings__general-auto-switch-theme" {{#if autoSwitchTheme}}checked{{/if}} />
|
||||||
|
<label for="settings__general-auto-switch-theme">{{res 'setGenAutoSwitchTheme'}}</label>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="settings__general-font-size">{{res 'setGenFontSize'}}:</label>
|
<label for="settings__general-font-size">{{res 'setGenFontSize'}}:</label>
|
||||||
<select class="settings__general-font-size settings__select input-base" id="settings__general-font-size">
|
<select class="settings__general-font-size settings__select input-base" id="settings__general-font-size">
|
||||||
|
@ -80,7 +84,9 @@
|
||||||
<select class="settings__general-titlebar-style settings__select input-base" id="settings__general-titlebar-style">
|
<select class="settings__general-titlebar-style settings__select input-base" id="settings__general-titlebar-style">
|
||||||
<option value="default" {{#ifeq titlebarStyle 'default'}}selected{{/ifeq}}>{{res 'setGenTitlebarStyleDefault'}}</option>
|
<option value="default" {{#ifeq titlebarStyle 'default'}}selected{{/ifeq}}>{{res 'setGenTitlebarStyleDefault'}}</option>
|
||||||
<option value="hidden" {{#ifeq titlebarStyle 'hidden'}}selected{{/ifeq}}>{{res 'setGenTitlebarStyleHidden'}}</option>
|
<option value="hidden" {{#ifeq titlebarStyle 'hidden'}}selected{{/ifeq}}>{{res 'setGenTitlebarStyleHidden'}}</option>
|
||||||
<option value="hidden-inset" {{#ifeq titlebarStyle 'hidden-inset'}}selected{{/ifeq}}>{{res 'setGenTitlebarStyleHiddenInset'}}</option>
|
{{#if supportsCustomTitleBarAndDraggableWindow}}
|
||||||
|
<option value="hidden-inset" {{#ifeq titlebarStyle 'hidden-inset'}}selected{{/ifeq}}>{{res 'setGenTitlebarStyleHiddenInset'}}</option>
|
||||||
|
{{/if}}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -150,6 +156,11 @@
|
||||||
{{#if minimizeOnClose}}checked{{/if}} />
|
{{#if minimizeOnClose}}checked{{/if}} />
|
||||||
<label for="settings__general-minimize">{{res 'setGenMinInstead'}}</label>
|
<label for="settings__general-minimize">{{res 'setGenMinInstead'}}</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" class="settings__input input-base settings__general-minimize-on-field-copy" id="settings__general-minimize-on-field-copy"
|
||||||
|
{{#if minimizeOnFieldCopy}}checked{{/if}} />
|
||||||
|
<label for="settings__general-minimize-on-field-copy">{{res 'setGenMinOnFieldCopy'}}</label>
|
||||||
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if canAutoType}}
|
{{#if canAutoType}}
|
||||||
<div>
|
<div>
|
||||||
|
@ -172,6 +183,85 @@
|
||||||
id="settings__general-use-group-icon-for-entries" {{#if useGroupIconForEntries}}checked{{/if}} />
|
id="settings__general-use-group-icon-for-entries" {{#if useGroupIconForEntries}}checked{{/if}} />
|
||||||
<label for="settings__general-use-group-icon-for-entries">{{res 'setGenUseGroupIconForEntries'}}</label>
|
<label for="settings__general-use-group-icon-for-entries">{{res 'setGenUseGroupIconForEntries'}}</label>
|
||||||
</div>
|
</div>
|
||||||
|
{{#if hasDeviceOwnerAuth}}
|
||||||
|
<div>
|
||||||
|
<label for="settings__general-device-owner-auth">{{res 'setGenTouchId'}}:</label>
|
||||||
|
<select class="settings__general-device-owner-auth settings__select input-base" id="settings__general-device-owner-auth">
|
||||||
|
<option value="" {{#unless deviceOwnerAuth}}selected{{/unless}}>{{res 'setGenTouchIdDisabled'}}</option>
|
||||||
|
<option value="memory" {{#ifeq deviceOwnerAuth 'memory'}}selected{{/ifeq}}>{{res 'setGenTouchIdMemory'}}</option>
|
||||||
|
<option value="file" {{#ifeq deviceOwnerAuth 'file'}}selected{{/ifeq}}>{{res 'setGenTouchIdFile'}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{{#if deviceOwnerAuth}}
|
||||||
|
<label for="settings__general-device-owner-auth-timeout">{{res 'setGenTouchIdPass'}}:</label>
|
||||||
|
<select class="settings__general-device-owner-auth-timeout settings__select input-base" id="settings__general-device-owner-auth-timeout">
|
||||||
|
<option value="1" {{#ifeq deviceOwnerAuthTimeout 1}}selected{{/ifeq}}>{{Res 'oneMinute'}}</option>
|
||||||
|
<option value="5" {{#ifeq deviceOwnerAuthTimeout 5}}selected{{/ifeq}}>{{#Res 'minutes'}}5{{/Res}}</option>
|
||||||
|
<option value="30" {{#ifeq deviceOwnerAuthTimeout 30}}selected{{/ifeq}}>{{#Res 'minutes'}}30{{/Res}}</option>
|
||||||
|
<option value="60" {{#ifeq deviceOwnerAuthTimeout 60}}selected{{/ifeq}}>{{Res 'oneHour'}}</option>
|
||||||
|
<option value="120" {{#ifeq deviceOwnerAuthTimeout 120}}selected{{/ifeq}}>{{#Res 'hours'}}2{{/Res}}</option>
|
||||||
|
<option value="480" {{#ifeq deviceOwnerAuthTimeout 480}}selected{{/ifeq}}>{{#Res 'hours'}}8{{/Res}}</option>
|
||||||
|
<option value="1440" {{#ifeq deviceOwnerAuthTimeout 1440}}selected{{/ifeq}}>{{Res 'oneDay'}}</option>
|
||||||
|
<option value="10080" {{#ifeq deviceOwnerAuthTimeout 10080}}selected{{/ifeq}}>{{Res 'oneWeek'}}</option>
|
||||||
|
{{#ifeq deviceOwnerAuth 'file'}}
|
||||||
|
<option value="43200" {{#ifeq deviceOwnerAuthTimeout 43200}}selected{{/ifeq}}>{{Res 'oneMonth'}}</option>
|
||||||
|
<option value="525600" {{#ifeq deviceOwnerAuthTimeout 525600}}selected{{/ifeq}}>{{Res 'oneYear'}}</option>
|
||||||
|
{{/ifeq}}
|
||||||
|
</select>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<h2 id="audit">{{res 'setGenAudit'}}</h2>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" class="settings__input input-base settings__general-audit-passwords"
|
||||||
|
id="settings__general-audit-passwords" {{#if auditPasswords}}checked{{/if}} />
|
||||||
|
<label for="settings__general-audit-passwords">{{res 'setGenAuditPasswords'}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" class="settings__input input-base settings__general-audit-password-entropy"
|
||||||
|
id="settings__general-audit-password-entropy" {{#if auditPasswordEntropy}}checked{{/if}} />
|
||||||
|
<label for="settings__general-audit-password-entropy">{{res 'setGenAuditPasswordEntropy'}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" class="settings__input input-base settings__general-exclude-pins-from-audit"
|
||||||
|
id="settings__general-exclude-pins-from-audit" {{#if excludePinsFromAudit}}checked{{/if}} />
|
||||||
|
<label for="settings__general-exclude-pins-from-audit">{{res 'setGenExcludePinsFromAudit'}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" class="settings__input input-base settings__general-check-passwords-on-hibp"
|
||||||
|
id="settings__general-check-passwords-on-hibp" {{#if checkPasswordsOnHIBP}}checked{{/if}} />
|
||||||
|
<label for="settings__general-check-passwords-on-hibp">
|
||||||
|
{{~#res 'setGenCheckPasswordsOnHIBP'~}}
|
||||||
|
<a href="{{hibpLink}}" rel="noreferrer noopener" target="_blank">Have I Been Pwned</a>
|
||||||
|
{{~/res~}}
|
||||||
|
</label>
|
||||||
|
<i class="fa fa-info-circle info-btn settings__general-toggle-help-hibp"></i>
|
||||||
|
<div class="settings__general-help-hibp hide">
|
||||||
|
{{~#res 'setGenHelpHIBP'~}}
|
||||||
|
<a href="{{hibpPrivacyLink}}" rel="noreferrer noopener" target="_blank">{{res 'setGenHelpHIBPLink'}}</a>
|
||||||
|
{{~/res~}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="settings__general-audit-password-age">{{res 'setGenAuditPasswordAge'}}:</label>
|
||||||
|
<select class="settings__select input-base settings__general-audit-password-age"
|
||||||
|
id="settings__general-audit-password-age">
|
||||||
|
<option value="0" {{#ifeq auditPasswordAge 0}}selected{{/ifeq}}>{{res 'setGenAuditPasswordAgeOff'}}</option>
|
||||||
|
<option value="1" {{#ifeq auditPasswordAge 1}}selected{{/ifeq}}>{{res 'setGenAuditPasswordAgeOneYear'}}</option>
|
||||||
|
<option value="2" {{#ifeq auditPasswordAge 2}}selected{{/ifeq}}>{{#res 'setGenAuditPasswordAgeYears'}}
|
||||||
|
2{{/res}}</option>
|
||||||
|
<option value="3" {{#ifeq auditPasswordAge 3}}selected{{/ifeq}}>{{#res 'setGenAuditPasswordAgeYears'}}
|
||||||
|
3{{/res}}</option>
|
||||||
|
<option value="5" {{#ifeq auditPasswordAge 5}}selected{{/ifeq}}>{{#res 'setGenAuditPasswordAgeYears'}}
|
||||||
|
5{{/res}}</option>
|
||||||
|
<option value="10" {{#ifeq auditPasswordAge 10}}selected{{/ifeq}}>{{#res 'setGenAuditPasswordAgeYears'}}
|
||||||
|
10{{/res}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 id="lock">{{res 'setGenLock'}}</h2>
|
<h2 id="lock">{{res 'setGenLock'}}</h2>
|
||||||
<div>
|
<div>
|
||||||
|
@ -183,6 +273,8 @@
|
||||||
<option value="15" {{#ifeq idleMinutes 15}}selected{{/ifeq}}>{{#res 'setGenLockMinutes'}}15{{/res}}</option>
|
<option value="15" {{#ifeq idleMinutes 15}}selected{{/ifeq}}>{{#res 'setGenLockMinutes'}}15{{/res}}</option>
|
||||||
<option value="30" {{#ifeq idleMinutes 30}}selected{{/ifeq}}>{{#res 'setGenLockMinutes'}}30{{/res}}</option>
|
<option value="30" {{#ifeq idleMinutes 30}}selected{{/ifeq}}>{{#res 'setGenLockMinutes'}}30{{/res}}</option>
|
||||||
<option value="60" {{#ifeq idleMinutes 60}}selected{{/ifeq}}>{{res 'setGenLockHour'}}</option>
|
<option value="60" {{#ifeq idleMinutes 60}}selected{{/ifeq}}>{{res 'setGenLockHour'}}</option>
|
||||||
|
<option value="180" {{#ifeq idleMinutes 180}}selected{{/ifeq}}>{{#res 'setGenLockHours'}}3{{/res}}</option>
|
||||||
|
<option value="360" {{#ifeq idleMinutes 360}}selected{{/ifeq}}>{{#res 'setGenLockHours'}}6{{/res}}</option>
|
||||||
<option value="720" {{#ifeq idleMinutes 720}}selected{{/ifeq}}>{{#res 'setGenLockHours'}}12{{/res}}</option>
|
<option value="720" {{#ifeq idleMinutes 720}}selected{{/ifeq}}>{{#res 'setGenLockHours'}}12{{/res}}</option>
|
||||||
<option value="1440" {{#ifeq idleMinutes 1440}}selected{{/ifeq}}>{{res 'setGenLockDay'}}</option>
|
<option value="1440" {{#ifeq idleMinutes 1440}}selected{{/ifeq}}>{{res 'setGenLockDay'}}</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -215,6 +307,12 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<h2 id="storage">{{res 'setGenStorage'}}</h2>
|
<h2 id="storage">{{res 'setGenStorage'}}</h2>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" class="settings__input input-base settings__general-disable-offline-storage" id="settings__general-disable-offline-storage"
|
||||||
|
{{#if disableOfflineStorage}}checked{{/if}} />
|
||||||
|
<label for="settings__general-disable-offline-storage">{{res 'setGenDisableOfflineStorage'}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{#each storageProviders as |prv|}}
|
{{#each storageProviders as |prv|}}
|
||||||
<h4 class="settings__general-storage-header"><input
|
<h4 class="settings__general-storage-header"><input
|
||||||
type="checkbox" id="settings__general-prv-check-{{prv.name}}" class="settings__general-prv-check"
|
type="checkbox" id="settings__general-prv-check-{{prv.name}}" class="settings__general-prv-check"
|
||||||
|
@ -228,6 +326,13 @@
|
||||||
<h2 id="advanced">{{res 'advanced'}}</h2>
|
<h2 id="advanced">{{res 'advanced'}}</h2>
|
||||||
<a class="settings__general-show-advanced">{{res 'setGenShowAdvanced'}}</a>
|
<a class="settings__general-show-advanced">{{res 'setGenShowAdvanced'}}</a>
|
||||||
<div class="settings__general-advanced hide">
|
<div class="settings__general-advanced hide">
|
||||||
|
{{#if canAutoType}}
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" class="settings__input input-base settings__general-use-legacy-autotype"
|
||||||
|
id="settings__general-use-legacy-autotype" {{#if useLegacyAutoType}}checked{{/if}} />
|
||||||
|
<label for="settings__general-use-legacy-autotype">{{res 'setGenUseLegacyAutoType'}}</label>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
{{#if devTools}}
|
{{#if devTools}}
|
||||||
<button class="btn-silent settings__general-dev-tools-link">{{res 'setGenDevTools'}}</button>
|
<button class="btn-silent settings__general-dev-tools-link">{{res 'setGenDevTools'}}</button>
|
||||||
<button class="btn-silent settings__general-try-beta-link">{{res 'setGenTryBeta'}}</button>
|
<button class="btn-silent settings__general-try-beta-link">{{res 'setGenTryBeta'}}</button>
|
||||||
|
|
|
@ -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