Compare commits
91 Commits
2.7-androi
...
2.7-java-7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39f6893742 | ||
|
|
7bf7b8981c | ||
|
|
09abe21fed | ||
|
|
6fae0fe425 | ||
|
|
0558176847 | ||
|
|
c553201cda | ||
|
|
665be9494b | ||
|
|
5ca81b4aa7 | ||
|
|
3cbb063926 | ||
|
|
d5551c8c8c | ||
|
|
9a40e52d53 | ||
|
|
6f0d768e69 | ||
|
|
40fdc8d248 | ||
|
|
6b9e1b8cb8 | ||
|
|
f41cdb8742 | ||
|
|
10c6d203b8 | ||
|
|
7d1aa9c9f4 | ||
|
|
c26281e3b7 | ||
|
|
f0b1f0c9e0 | ||
|
|
9682efc7c9 | ||
|
|
1264cad377 | ||
|
|
d185a0af14 | ||
|
|
4275a6cc61 | ||
|
|
c94ff429e8 | ||
|
|
00744cb264 | ||
|
|
7202fe6d1d | ||
|
|
63b4d9cd2e | ||
|
|
36a7c7f423 | ||
|
|
c2c4fb18bf | ||
|
|
3fc8acba70 | ||
|
|
f5c0c4d787 | ||
|
|
86775f1c75 | ||
|
|
2bb190f49a | ||
|
|
77c4a2af46 | ||
|
|
3da82d30b1 | ||
|
|
97532fdce6 | ||
|
|
fe63a2756a | ||
|
|
928b617ed0 | ||
|
|
18ecc41b39 | ||
|
|
a6e9e89ace | ||
|
|
0b7494ecbf | ||
|
|
8377c9c615 | ||
|
|
37a7cfa530 | ||
|
|
978b758079 | ||
|
|
38f09021b3 | ||
|
|
7455fba55e | ||
|
|
8cd9755616 | ||
|
|
46d301df94 | ||
|
|
e639137304 | ||
|
|
7c83a62f91 | ||
|
|
513840e2c4 | ||
|
|
8f7faa9e4e | ||
|
|
16cdcda94b | ||
|
|
400ebe59db | ||
|
|
476a4046e7 | ||
|
|
3403449ca2 | ||
|
|
596ace51ea | ||
|
|
80b5fcd785 | ||
|
|
a16bc9a318 | ||
|
|
462dd4e89b | ||
|
|
e5ff374a9c | ||
|
|
954c4f8d63 | ||
|
|
529f1feace | ||
|
|
5cdff6f155 | ||
|
|
81358c16f9 | ||
|
|
4a555748cd | ||
|
|
698566a914 | ||
|
|
64a69856ac | ||
|
|
f76de9520c | ||
|
|
4b3662bbe9 | ||
|
|
b25130f4d2 | ||
|
|
d6617563fc | ||
|
|
8d24ec3250 | ||
|
|
bbcc250a5c | ||
|
|
4abb50ad9b | ||
|
|
30dac64d5d | ||
|
|
e4e2aaad95 | ||
|
|
835acf45eb | ||
|
|
c3f6796833 | ||
|
|
86f4e8ec06 | ||
|
|
0dddcef28e | ||
|
|
cc583c789d | ||
|
|
42d78da74e | ||
|
|
b5040a7786 | ||
|
|
b1698ee339 | ||
|
|
8ffc0ae350 | ||
|
|
8276d2f4e5 | ||
|
|
11cf86bc73 | ||
|
|
5084511404 | ||
|
|
fffec56d4e | ||
|
|
9a0828c1eb |
28
.dockerignore
Normal file
@@ -0,0 +1,28 @@
|
||||
# OS-Specific junk.
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IntelliJ
|
||||
.idea
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
# Xcode IDE
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
|
||||
# Generated
|
||||
/platform-darwin/Resources/Media/Images.xcassets/
|
||||
/platform-darwin/Podfile.lock
|
||||
/platform-darwin/Pods/
|
||||
|
||||
# Gradle
|
||||
build
|
||||
.gradle
|
||||
local.properties
|
||||
/gradle/builds
|
||||
/platform-android/.externalNativeBuild
|
||||
|
||||
# Git
|
||||
.git
|
||||
1
.gitignore
vendored
@@ -7,6 +7,7 @@ Thumbs.db
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
out
|
||||
|
||||
# Xcode IDE
|
||||
xcuserdata/
|
||||
|
||||
@@ -6,12 +6,13 @@ build_project:
|
||||
stage: build
|
||||
script:
|
||||
- "( brew bundle )"
|
||||
- "( ./lib/bin/build_libsodium-macos )"
|
||||
- "( ./lib/bin/build_libsodium-macos clean && ./lib/bin/build_libsodium-macos )"
|
||||
- "( ./lib/bin/build_libjson-c-macos clean && ./lib/bin/build_libjson-c-macos )"
|
||||
- "( cd ./platform-independent/c/cli && ./clean && targets=all ./build && ./mpw-tests && ./mpw-cli-tests )"
|
||||
- "( cd ./gradle && ./gradlew --info clean test )"
|
||||
- "( cd ./gradle && ./gradlew --stacktrace clean test )"
|
||||
- "( xcodebuild -workspace platform-darwin/MasterPassword.xcworkspace -configuration 'Test' -scheme 'MasterPassword iOS' -sdk iphonesimulator clean build )"
|
||||
- "( xcodebuild -workspace platform-darwin/MasterPassword.xcworkspace -configuration 'Test' -scheme 'MasterPassword macOS' clean build )"
|
||||
tags:
|
||||
- brew
|
||||
- java
|
||||
- java_9
|
||||
- xcode_9
|
||||
|
||||
1
Brewfile
@@ -1,5 +1,6 @@
|
||||
brew "libsodium"
|
||||
brew "json-c"
|
||||
|
||||
brew "libtool"
|
||||
brew "automake"
|
||||
brew "autoconf"
|
||||
|
||||
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM debian:stable-slim
|
||||
|
||||
# For i386
|
||||
#FROM i386/debian:stable-slim
|
||||
#ENTRYPOINT ["linux32", "--"]
|
||||
|
||||
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199
|
||||
RUN mkdir -p /usr/share/man/man1
|
||||
|
||||
RUN apt-get update && apt-get install -y default-jdk-headless git-core bash libtool automake autoconf make g++
|
||||
RUN git clone --depth=3 $(: --shallow-submodules) --recurse-submodules https://gitlab.com/MasterPassword/MasterPassword.git /mpw
|
||||
RUN cd /mpw/gradle && ./gradlew -i clean build
|
||||
@@ -122,7 +122,7 @@ The `master-password` on the other hand, is only a simple phrase, which means it
|
||||
|
||||
5. I have another question.
|
||||
|
||||
Please don't hesitate to [get in touch](#support), we're more than happy to answer all your Master Password questions. Any problems or suggestions can be reported [as GitHub issues](https://github.com/Lyndir/MasterPassword/issues).
|
||||
Please don't hesitate to [get in touch](#support), we're more than happy to answer all your Master Password questions. Any problems or suggestions can be reported [as GitLab issues](https://gitlab.com/MasterPassword/MasterPassword/issues/).
|
||||
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ Note that in order to build the Android application, you will need to have the A
|
||||
|
||||
Go into the `platform-independent/cli-c` directory and run `./build`. The native command-line client will then be built.
|
||||
|
||||
For detailed instructions, see [the native CLI instructions](platform-independent/cli-c/README.md).
|
||||
For detailed instructions, see [the native CLI instructions](platform-independent/c/README.md).
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
9
gradle/.idea/codeStyleSettings.xml
generated
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectCodeStyleSettingsManager">
|
||||
<option name="PER_PROJECT_SETTINGS">
|
||||
<value />
|
||||
</option>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Lhunath" />
|
||||
</component>
|
||||
</project>
|
||||
7
gradle/.idea/copyright/GPLv3.xml
generated
@@ -1,7 +0,0 @@
|
||||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="keyword" value="Copyright|License|WARRANTY" />
|
||||
<option name="myName" value="GPLv3" />
|
||||
<option name="notice" value="This file is part of &#36;project.name. Copyright (c) &#36;today.year. &#36;project.name is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. &#36;project.name is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You can find a copy of the GNU General Public License in the LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>." />
|
||||
</copyright>
|
||||
</component>
|
||||
13
gradle/.idea/copyright/profiles_settings.xml
generated
@@ -1,13 +0,0 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings>
|
||||
<module2copyright>
|
||||
<element module="masterpassword" copyright="Master Password" />
|
||||
</module2copyright>
|
||||
<LanguageOptions name="__TEMPLATE__">
|
||||
<option name="block" value="false" />
|
||||
<option name="separateBefore" value="true" />
|
||||
<option name="separateAfter" value="true" />
|
||||
<option name="filler" value="=" />
|
||||
</LanguageOptions>
|
||||
</settings>
|
||||
</component>
|
||||
@@ -1,9 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="projectProfile" value="Lhunath" />
|
||||
<option name="useProjectProfile" value="false" />
|
||||
<option name="PROJECT_PROFILE" value="Lhunath" />
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
34
gradle/.idea/misc.xml
generated
@@ -1,45 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<type id="jpa" />
|
||||
<type id="web" />
|
||||
</component>
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/../../opal/pom.xml" />
|
||||
<option value="$PROJECT_DIR$/../../pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="NullableNotNullManager">
|
||||
<option name="myDefaultNullable" value="javax.annotation.Nullable" />
|
||||
<option name="myDefaultNotNull" value="javax.annotation.Nonnull" />
|
||||
<option name="myNullables">
|
||||
<value>
|
||||
<list size="4">
|
||||
<list size="9">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||
<item index="4" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
|
||||
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
|
||||
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
|
||||
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
|
||||
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myNotNulls">
|
||||
<value>
|
||||
<list size="4">
|
||||
<list size="9">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
||||
<item index="4" class="java.lang.String" itemvalue="javax.validation.constraints.NotNull" />
|
||||
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
|
||||
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
|
||||
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
|
||||
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/classes" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="10" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ThriftCompiler">
|
||||
<compilers />
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
28
gradle/.idea/runConfigurations/Android.xml
generated
@@ -1,28 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Android" type="AndroidRunConfigurationType" factoryName="Android App">
|
||||
<module name="android" />
|
||||
<option name="DEPLOY" value="true" />
|
||||
<option name="ARTIFACT_NAME" value="" />
|
||||
<option name="PM_INSTALL_OPTIONS" value="" />
|
||||
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
|
||||
<option name="MODE" value="default_activity" />
|
||||
<option name="PREFERRED_AVD" value="" />
|
||||
<option name="CLEAR_LOGCAT" value="false" />
|
||||
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
|
||||
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
|
||||
<option name="FORCE_STOP_RUNNING_APP" value="true" />
|
||||
<option name="TARGET_SELECTION_MODE" value="SHOW_DIALOG" />
|
||||
<option name="USE_LAST_SELECTED_DEVICE" value="false" />
|
||||
<option name="PREFERRED_AVD" value="" />
|
||||
<option name="DEBUGGER_TYPE" value="Java" />
|
||||
<Java />
|
||||
<Profilers>
|
||||
<option name="ENABLE_ADVANCED_PROFILING" value="false" />
|
||||
<option name="SUPPORT_LIB_ENABLED" value="true" />
|
||||
<option name="INSTRUMENTATION_ENABLED" value="true" />
|
||||
</Profilers>
|
||||
<option name="DEEP_LINK" value="" />
|
||||
<option name="ACTIVITY_CLASS" value="" />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
||||
3
gradle/.idea/scopes/masterpassword.xml
generated
@@ -1,3 +0,0 @@
|
||||
<component name="DependencyValidationManager">
|
||||
<scope name="masterpassword" pattern="com.lyndir.masterpassword..*" />
|
||||
</component>
|
||||
@@ -2,11 +2,11 @@ To build a release distribution:
|
||||
|
||||
Desktop:
|
||||
|
||||
STORE_PW=$(mpw masterpassword.keystore) KEY_PW_DESKTOP=$(mpw masterpassword-desktop) gradle clean masterpassword-gui:shadowJar
|
||||
STORE_PW=$(mpw masterpassword.keystore) KEY_PW_DESKTOP=$(mpw masterpassword-desktop) gradle --no-daemon clean masterpassword-gui:shadowJar
|
||||
|
||||
Android:
|
||||
|
||||
STORE_PW=$(mpw masterpassword.keystore) KEY_PW_ANDROID=$(mpw masterpassword-android) gradle clean masterpassword-android:assembleRelease
|
||||
STORE_PW=$(mpw masterpassword.keystore) KEY_PW_ANDROID=$(mpw masterpassword-android) gradle --no-daemon clean masterpassword-android:assembleRelease
|
||||
|
||||
|
||||
Note:
|
||||
|
||||
@@ -2,7 +2,7 @@ allprojects {
|
||||
apply plugin: 'findbugs'
|
||||
|
||||
group = 'com.lyndir.masterpassword'
|
||||
version = 'GIT-SNAPSHOT'
|
||||
version = '2.7.7'
|
||||
|
||||
tasks.withType( JavaCompile ) {
|
||||
options.encoding = 'UTF-8'
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
org.gradle.daemon=true
|
||||
org.gradle.configureondemand=true
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.enableD8.desugaring=true
|
||||
|
||||
@@ -4,13 +4,14 @@ class Mpw < Formula
|
||||
url "https://masterpassword.app/mpw-2.6-cli-5-0-g344771db.tar.gz"
|
||||
version "2.6-cli-5"
|
||||
sha256 "954c07b1713ecc2b30a07bead9c11e6204dd774ca67b5bdf7d2d6ad1c4eec170"
|
||||
head "https://github.com/Lyndir/MasterPassword.git"
|
||||
revision 1
|
||||
head "https://gitlab.com/MasterPassword/MasterPassword.git"
|
||||
|
||||
bottle do
|
||||
cellar :any
|
||||
sha256 "ae8b265936797778a7cde788377eed89d9eacd267755a0b1186790057a10ff3b" => :high_sierra
|
||||
sha256 "b8a106c3c84ff939e928613d4a6ccf7b5234e40ebae1edf15e3cac52d8c2e5ea" => :sierra
|
||||
sha256 "9b58425b028a2598932474e1d0c17c13aad57e0a53ae7308c1b38404da8f3331" => :el_capitan
|
||||
sha256 "46677cf8649983d5b77103d2ca56d9ad3697808ecc406f626a3462a089f932da" => :high_sierra
|
||||
sha256 "19bf22915b3c534ad3ee6f1dfc20f142d53ae6c0c88757ae2632b7b1daa6667f" => :sierra
|
||||
sha256 "7090c3d31289d2ac5529bd0a6bae2632a36ba7fcd4bb7974248bb36a15f67c7e" => :el_capitan
|
||||
end
|
||||
|
||||
option "without-json-c", "Disable JSON configuration support"
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
rootProject.name = 'masterpassword'
|
||||
|
||||
def local = new Properties();
|
||||
try {
|
||||
local.load(file('local.properties').newDataInputStream())
|
||||
} catch (FileNotFoundException ignored) {
|
||||
}
|
||||
def local = new Properties()
|
||||
def localFile = file( 'local.properties' )
|
||||
localFile.exists() && local.load( localFile.newDataInputStream() )
|
||||
|
||||
include 'masterpassword-core'
|
||||
project(':masterpassword-core').projectDir = new File( '../platform-independent/c/core' )
|
||||
project( ':masterpassword-core' ).projectDir = new File( '../platform-independent/c/core' )
|
||||
|
||||
include 'masterpassword-algorithm'
|
||||
project(':masterpassword-algorithm').projectDir = new File( '../platform-independent/java/algorithm' )
|
||||
project( ':masterpassword-algorithm' ).projectDir = new File( '../platform-independent/java/algorithm' )
|
||||
|
||||
include 'masterpassword-model'
|
||||
project(':masterpassword-model').projectDir = new File( '../platform-independent/java/model' )
|
||||
project( ':masterpassword-model' ).projectDir = new File( '../platform-independent/java/model' )
|
||||
|
||||
include 'masterpassword-tests'
|
||||
project(':masterpassword-tests').projectDir = new File( '../platform-independent/java/tests' )
|
||||
project( ':masterpassword-tests' ).projectDir = new File( '../platform-independent/java/tests' )
|
||||
|
||||
include 'masterpassword-gui'
|
||||
project(':masterpassword-gui').projectDir = new File( '../platform-independent/java/gui' )
|
||||
project( ':masterpassword-gui' ).projectDir = new File( '../platform-independent/java/gui' )
|
||||
|
||||
if (local.containsKey('sdk.dir')) {
|
||||
if (local.containsKey( 'sdk.dir' ) && file( local.getProperty( 'sdk.dir' ) ).exists()) {
|
||||
include 'masterpassword-android'
|
||||
project(':masterpassword-android').projectDir = new File( '../platform-android' )
|
||||
project( ':masterpassword-android' ).projectDir = new File( '../platform-android' )
|
||||
} else {
|
||||
logger.warn( "Skipping masterpassword-android since sdk.dir is not defined in local.properties." )
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# Your build script should simply source this script, optionally override any build hooks and then invoke `build`.
|
||||
# The build product should be available under `build-<platform>~/out`, under the library path.
|
||||
#
|
||||
#
|
||||
# Hook lifecycle:
|
||||
# - build
|
||||
# - initialize
|
||||
@@ -24,6 +24,7 @@
|
||||
# target_prepare() { make -s distclean; }
|
||||
# target_configure() { _target_configure "$@" --enable-minimal; }
|
||||
set -e
|
||||
PATH+=:/usr/local/bin
|
||||
|
||||
# needs <binary> ...
|
||||
#
|
||||
@@ -31,8 +32,15 @@ set -e
|
||||
needs() { _needs "$@"; }
|
||||
_needs() {
|
||||
local failed=0
|
||||
for tool; do
|
||||
hash "$tool" || { echo >&2 "Missing: $tool. Please install this tool."; (( failed++ )); }
|
||||
for spec; do
|
||||
IFS=: read pkg tools <<< "$spec"
|
||||
IFS=, read -a tools <<< "${tools:-$pkg}"
|
||||
for tool in "${tools[@]}"; do
|
||||
hash "$tool" 2>/dev/null && continue 2
|
||||
done
|
||||
|
||||
echo >&2 "Missing: $pkg. Please install this package."
|
||||
(( failed++ ))
|
||||
done
|
||||
|
||||
return $failed
|
||||
@@ -50,10 +58,16 @@ _initialize() {
|
||||
#
|
||||
# Check if all tools needed for the default implementations are available.
|
||||
#
|
||||
# By default, this will check for `automake` (for aclocal) and `autoconf` (for autoreconf).
|
||||
# By default, this will check for `libtool` (for libtoolize), `automake` (for aclocal), `autoconf` (for autoreconf) and make.
|
||||
initialize_needs() { _initialize_needs "$@"; }
|
||||
_initialize_needs() {
|
||||
needs automake autoconf
|
||||
if [[ $platform = windows ]]; then
|
||||
needs cmd
|
||||
export VSINSTALLDIR="${VSINSTALLDIR:-$(cd "$(cygpath -F 0x002a)/Microsoft Visual Studio"/*/*/Common7/.. && pwd)}"
|
||||
[[ -e "$VSINSTALLDIR/Common7/Tools/VsMSBuildCmd.bat" ]] || { echo >&2 "Missing: msbuild. Please install 'Build Tools for Visual Studio'. See https://visualstudio.microsoft.com/downloads/?q=build+tools"; return 1; }
|
||||
else
|
||||
needs libtool:libtoolize,glibtoolize automake autoconf make
|
||||
fi
|
||||
}
|
||||
|
||||
# clean <prefix> <platform>
|
||||
@@ -63,9 +77,15 @@ _initialize_needs() {
|
||||
# By default, this will wipe the prefix, run `make distclean` and `git clean -fdx`.
|
||||
clean() { _clean "$@"; }
|
||||
_clean() {
|
||||
if [[ $platform = windows ]]; then
|
||||
printf '"%%VSINSTALLDIR%%\Common7\Tools\VsMSBuildCmd.bat" && msbuild /t:Clean' > .clean.bat
|
||||
cmd //c .clean.bat
|
||||
rm -f .clean.bat
|
||||
elif [[ -e Makefile ]] && make -s distclean; then :
|
||||
elif [[ -e .git ]] && git clean -fdx; then :
|
||||
fi
|
||||
|
||||
rm -rf "$prefix"
|
||||
[[ ! -e Makefile ]] || make -s distclean
|
||||
[[ ! -e .git ]] || git clean -fdx
|
||||
}
|
||||
|
||||
# prepare <prefix> <platform> [ <arch> ... ]
|
||||
@@ -91,7 +111,7 @@ _prepare_clean() {
|
||||
local prefix=$1 platform=$2; shift 2
|
||||
|
||||
rm -rf "$prefix"
|
||||
install -d "$prefix"
|
||||
install -d "$prefix/out"
|
||||
}
|
||||
|
||||
# prepare_config <prefix> <platform> [ <arch> ... ]
|
||||
@@ -103,7 +123,14 @@ prepare_config() { _prepare_config "$@"; }
|
||||
_prepare_config() {
|
||||
local prefix=$1 platform=$2; shift 2
|
||||
|
||||
[[ -e configure ]] || autoreconf --verbose --install --symlink 2> >(sed 's/^\([^:]*\):[0-9]\{1,\}: /\1: /')
|
||||
[[ -e "$prefix/out/.prepared" ]] && return
|
||||
|
||||
if [[ $platform = windows ]]; then :
|
||||
else
|
||||
autoreconf --verbose --install --force 2> >(sed 's/^\([^:]*\):[0-9]\{1,\}: /\1: /')
|
||||
fi
|
||||
|
||||
touch "$prefix/out/.prepared"
|
||||
}
|
||||
|
||||
# target <prefix> <platform> <arch>
|
||||
@@ -127,19 +154,27 @@ target_prepare() { _target_prepare "$@"; }
|
||||
_target_prepare() {
|
||||
local prefix=$1 platform=$2 arch=$3; shift 3
|
||||
|
||||
[[ ! -e Makefile ]] || make -s clean
|
||||
if [[ $platform = windows ]]; then :
|
||||
else
|
||||
[[ ! -e Makefile ]] || make -s clean
|
||||
fi
|
||||
}
|
||||
|
||||
# target_configure <prefix> <platform> <arch> [ <args> ... ]
|
||||
#
|
||||
# Configure the library for building the target.
|
||||
#
|
||||
# By default, this will run `./configure --host=<host> --prefix=<prefix>/<arch> --disable-shared <args>`.
|
||||
# By default, this will run `./configure --host=<host> --prefix=<prefix>/<arch> <args>`.
|
||||
# By default, some platform-specific arguments will be passed in as well as
|
||||
# --enable-pic --disable-pie to ensure the resulting library can be linked again.
|
||||
target_configure() { _target_configure "$@"; }
|
||||
_target_configure() {
|
||||
local prefix=$1 platform=$2 arch=$3; shift 3
|
||||
|
||||
case "$platform" in
|
||||
'windows')
|
||||
return
|
||||
;;
|
||||
'android')
|
||||
host=( "$SDKROOT"/*-android* ) host=${host##*/}
|
||||
|
||||
@@ -153,7 +188,7 @@ _target_configure() {
|
||||
;;
|
||||
esac
|
||||
|
||||
./configure ${host:+--host="$host"} --prefix="$prefix/$arch" "$@"
|
||||
./configure ${host:+--host="$host"} --enable-pic --disable-pie --prefix="$prefix/$arch" "$@"
|
||||
}
|
||||
|
||||
# target_build <prefix> <platform> <arch>
|
||||
@@ -164,10 +199,16 @@ _target_configure() {
|
||||
target_build() { _target_build "$@"; }
|
||||
_target_build() {
|
||||
local prefix=$1 platform=$2 arch=$3; shift 3
|
||||
#make -j3 check
|
||||
|
||||
cores=$(getconf NPROCESSORS_ONLN 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null ||:)
|
||||
make -j"${cores:-3}" install
|
||||
if [[ $platform = windows ]]; then
|
||||
# I cannot for the life of me figure out how to pass this command directly into cmd.
|
||||
printf '"%%VSINSTALLDIR%%\Common7\Tools\VsMSBuildCmd.bat" && msbuild /t:Rebuild /p:Configuration=Release;Platform=%s;OutDir=%s' "$arch" "$(cygpath -w "${prefix##$PWD/}/$arch/")" > .build.bat
|
||||
cmd //c .build.bat
|
||||
rm -f .build.bat
|
||||
else
|
||||
local cores=$(getconf NPROCESSORS_ONLN 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null ||:)
|
||||
make -j"${cores:-3}" install
|
||||
fi
|
||||
}
|
||||
|
||||
# finalize <prefix> <platform> [ <arch> ... ]
|
||||
@@ -188,21 +229,34 @@ _finalize() {
|
||||
finalize_merge() { _finalize_merge "$@"; }
|
||||
_finalize_merge() {
|
||||
local prefix=$1 platform=$2; shift 2
|
||||
local archs=( "$@" )
|
||||
|
||||
mv -f -- "$prefix/$1/include" "$prefix/out/"
|
||||
[[ -e "$prefix/$archs/include" ]] && mv -f -- "$prefix/$archs/include" "$prefix/out/"
|
||||
|
||||
mkdir -p "$prefix/out/lib"
|
||||
install -d "$prefix/out/lib"
|
||||
case "$platform" in
|
||||
'linux')
|
||||
for arch in "${archs[@]}"; do
|
||||
install -d "$prefix/out/lib/$arch"
|
||||
install -p "$prefix/$arch/lib/"*.a "$prefix/out/lib/$arch/"
|
||||
done
|
||||
;;
|
||||
'windows')
|
||||
for arch in "${archs[@]}"; do
|
||||
install -d "$prefix/out/lib/$arch"
|
||||
install -p "$prefix/$arch/"*.lib "$prefix/out/lib/$arch/"
|
||||
done
|
||||
;;
|
||||
'macos'|'ios')
|
||||
for lib in "$prefix/$1/lib/"*; do
|
||||
for lib in "$prefix/$archs/lib/"*; do
|
||||
if lipo -info "$lib" >/dev/null 2>&1; then
|
||||
local lib=("${lib##*/}") libs=("${@/#/$prefix/}") libs=("${libs[@]/%//lib/$lib}")
|
||||
local lib=("${lib##*/}") libs=("${archs[@]/#/$prefix/}") libs=("${libs[@]/%//lib/$lib}")
|
||||
lipo -create "${libs[@]}" -output "$prefix/out/lib/$lib"
|
||||
fi
|
||||
done
|
||||
;;
|
||||
'android')
|
||||
for arch; do
|
||||
for arch in "${archs[@]}"; do
|
||||
local abi=$arch
|
||||
case "$arch" in
|
||||
'arm') abi='armeabi-v7a' ;;
|
||||
@@ -224,7 +278,10 @@ _finalize_merge() {
|
||||
# By default, this will run `make clean`.
|
||||
finalize_clean() { _finalize_clean "$@"; }
|
||||
_finalize_clean() {
|
||||
[[ ! -e Makefile ]] || make -s clean
|
||||
if [[ $platform = windows ]]; then :
|
||||
else
|
||||
[[ ! -e Makefile ]] || make -s clean
|
||||
fi
|
||||
}
|
||||
|
||||
# build <name> [<platform>]
|
||||
@@ -247,6 +304,8 @@ _build() {
|
||||
'macos') archs=( 'x86_64' ) ;;
|
||||
'ios') archs=( 'i386' 'x86_64' 'armv7' 'armv7s' 'arm64' ) ;;
|
||||
'android') archs=( 'arm' 'arm64' 'x86' 'x86_64' ) ;;
|
||||
'windows') archs=( 'Win32' 'x64' ) ;;
|
||||
*) archs=( 'i386' 'x86_64' ) ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -270,6 +329,8 @@ _build() {
|
||||
|
||||
# Set up a base environment for the platform.
|
||||
case "$platform" in
|
||||
'windows')
|
||||
;;
|
||||
'macos')
|
||||
SDKROOT="$(xcrun --show-sdk-path --sdk macosx)"
|
||||
export PATH="$(xcrun --show-sdk-platform-path --sdk macosx)/usr/bin:$PATH"
|
||||
@@ -319,4 +380,3 @@ _build() {
|
||||
|
||||
finalize "$prefix" "$platform" "${archs[@]}"
|
||||
}
|
||||
|
||||
|
||||
8
lib/bin/build_libjson-c-linux
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
source "${BASH_SOURCE%/*}/build_lib"
|
||||
|
||||
autoreconf() {
|
||||
command autoreconf -Iautoconf-archive/m4 "$@"
|
||||
}
|
||||
|
||||
build libjson-c linux
|
||||
8
lib/bin/build_libjson-c-windows
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
source "${BASH_SOURCE%/*}/build_lib"
|
||||
|
||||
autoreconf() {
|
||||
command autoreconf -Iautoconf-archive/m4 "$@"
|
||||
}
|
||||
|
||||
build libjson-c windows
|
||||
4
lib/bin/build_libsodium-linux
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
source "${BASH_SOURCE%/*}/build_lib"
|
||||
|
||||
build libsodium linux
|
||||
13
lib/bin/build_libsodium-windows
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
source "${BASH_SOURCE%/*}/build_lib"
|
||||
|
||||
finalize_merge() {
|
||||
local prefix=$1 platform=$2; shift 2
|
||||
local archs=( "$@" )
|
||||
|
||||
cp -a "src/libsodium/include" "$prefix/out"
|
||||
|
||||
_finalize_merge "$prefix" "$platform" "${archs[@]}"
|
||||
}
|
||||
|
||||
build libsodium windows
|
||||
@@ -13,12 +13,12 @@ add_library( mpw SHARED
|
||||
|
||||
add_library( sodium SHARED IMPORTED )
|
||||
set_target_properties( sodium PROPERTIES IMPORTED_LOCATION "${PROJECT_SOURCE_DIR}/../lib/libsodium/build-android~/out/lib/${ANDROID_ABI}/libsodium.so" )
|
||||
target_include_directories( mpw PRIVATE "${PROJECT_SOURCE_DIR}/../lib/libsodium/build-android~/out" )
|
||||
target_include_directories( mpw PRIVATE "${PROJECT_SOURCE_DIR}/../lib/libsodium/build-android~/out/include" )
|
||||
target_compile_definitions( mpw PRIVATE -DMPW_SODIUM=1 )
|
||||
target_link_libraries( mpw PRIVATE sodium )
|
||||
|
||||
add_library( json-c SHARED IMPORTED )
|
||||
set_target_properties( json-c PROPERTIES IMPORTED_LOCATION "${PROJECT_SOURCE_DIR}/../lib/libjson-c/build-android~/out/lib/${ANDROID_ABI}/libjson-c.so" )
|
||||
target_include_directories( mpw PRIVATE "${PROJECT_SOURCE_DIR}/../lib/libjson-c/build-android~/out" )
|
||||
target_include_directories( mpw PRIVATE "${PROJECT_SOURCE_DIR}/../lib/libjson-c/build-android~/out/include" )
|
||||
target_compile_definitions( mpw PRIVATE -DMPW_JSON=1 )
|
||||
target_link_libraries( mpw PRIVATE json-c )
|
||||
|
||||
@@ -22,8 +22,8 @@ android {
|
||||
}
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs "$projectDir/../lib/libsodium/build-android~/out/lib",
|
||||
"$projectDir/../lib/libjson-c/build-android~/out/lib"
|
||||
jniLibs.srcDirs "$rootDir/../lib/libsodium/build-android~/out/lib",
|
||||
"$rootDir/../lib/libjson-c/build-android~/out/lib"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,11 +55,23 @@ dependencies {
|
||||
}
|
||||
|
||||
preBuild {
|
||||
dependsOn task( type: Exec, 'buildLibSodium', {
|
||||
commandLine "$projectDir/../lib/bin/build_libsodium-android"
|
||||
dependsOn task( type: Exec, 'build_libsodium-android', {
|
||||
commandLine 'bash', "$rootDir/../lib/bin/build_libsodium-android"
|
||||
environment 'ANDROID_NDK_HOME', android.ndkDirectory
|
||||
} )
|
||||
dependsOn task( type: Exec, 'buildLibJson-c', {
|
||||
commandLine "$projectDir/../lib/bin/build_libjson-c-android"
|
||||
dependsOn task( type: Exec, 'build_libjson-c-android', {
|
||||
commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-android"
|
||||
environment 'ANDROID_NDK_HOME', android.ndkDirectory
|
||||
} )
|
||||
}
|
||||
|
||||
clean {
|
||||
dependsOn task( type: Exec, 'clean_libsodium-android', {
|
||||
commandLine 'bash', "$rootDir/../lib/bin/build_libsodium-android", 'clean'
|
||||
environment 'ANDROID_NDK_HOME', android.ndkDirectory
|
||||
} )
|
||||
dependsOn task( type: Exec, 'clean_libjson-c-android', {
|
||||
commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-android", 'clean'
|
||||
environment 'ANDROID_NDK_HOME', android.ndkDirectory
|
||||
} )
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
DA1554ED20B3928E00EA92C5 /* mpw-util.h */,
|
||||
);
|
||||
name = core;
|
||||
path = ../platform-independent/c/core/src;
|
||||
path = "../platform-independent/c/core/src";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
@@ -129,8 +129,9 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"/Library/Java/JavaVirtualMachines/jdk1.8.0_161.jdk/Contents/Home/include/**",
|
||||
../lib/libsodium/src/libsodium/include,
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-ios~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-ios~/out/include\"",
|
||||
);
|
||||
JAVA_HOME = /Library/Java/JavaVirtualMachines/jdk1.8.0_161.jdk/Contents/Home;
|
||||
OTHER_CFLAGS = (
|
||||
@@ -144,8 +145,9 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"/Library/Java/JavaVirtualMachines/jdk1.8.0_161.jdk/Contents/Home/include/**",
|
||||
../lib/libsodium/src/libsodium/include,
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-ios~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-ios~/out/include\"",
|
||||
);
|
||||
JAVA_HOME = /Library/Java/JavaVirtualMachines/jdk1.8.0_161.jdk/Contents/Home;
|
||||
OTHER_CFLAGS = (
|
||||
|
||||
@@ -1783,7 +1783,7 @@
|
||||
93D39CF7DB942C69D1C5D6BE /* mpw-util.h */,
|
||||
);
|
||||
name = core;
|
||||
path = ../platform-independent/c/core/src;
|
||||
path = "../platform-independent/c/core/src";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DA0CC4F41EAB99BA009A8ED9 /* Resources */ = {
|
||||
@@ -4309,12 +4309,6 @@
|
||||
GCC_WARN_UNUSED_LABEL = YES;
|
||||
GCC_WARN_UNUSED_VALUE = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"\"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/$(PLATFORM_NAME)/include\"",
|
||||
"\"$(PROJECT_DIR)/External/libsodium/libsodium-ios/include\"",
|
||||
"\"$(PROJECT_DIR)/External/libjson-c/libjson-c-ios/include\"",
|
||||
"$(inherited)",
|
||||
);
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
@@ -4348,13 +4342,18 @@
|
||||
GCC_C_LANGUAGE_STANDARD = c11;
|
||||
GCC_PREFIX_HEADER = "Source/MasterPassword-Prefix.pch";
|
||||
GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-ios~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-ios~/out/include\"",
|
||||
);
|
||||
INFOPLIST_FILE = "Source/iOS/MasterPassword-Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../lib/libsodium/build-ios~/out/lib",
|
||||
"$(PROJECT_DIR)/../lib/libjson-c/build-ios~/out/lib",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-ios~/out/lib\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-ios~/out/lib\"",
|
||||
);
|
||||
OTHER_CFLAGS = (
|
||||
"-DMPW_SODIUM=1",
|
||||
@@ -4524,12 +4523,6 @@
|
||||
GCC_WARN_UNUSED_LABEL = YES;
|
||||
GCC_WARN_UNUSED_VALUE = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"\"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/$(PLATFORM_NAME)/include\"",
|
||||
"\"$(PROJECT_DIR)/External/libsodium/libsodium-ios/include\"",
|
||||
"\"$(PROJECT_DIR)/External/libjson-c/libjson-c-ios/include\"",
|
||||
"$(inherited)",
|
||||
);
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -4635,12 +4628,6 @@
|
||||
GCC_WARN_UNUSED_LABEL = YES;
|
||||
GCC_WARN_UNUSED_VALUE = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"\"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/$(PLATFORM_NAME)/include\"",
|
||||
"\"$(PROJECT_DIR)/External/libsodium/libsodium-ios/include\"",
|
||||
"\"$(PROJECT_DIR)/External/libjson-c/libjson-c-ios/include\"",
|
||||
"$(inherited)",
|
||||
);
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
@@ -4673,13 +4660,18 @@
|
||||
GCC_C_LANGUAGE_STANDARD = c11;
|
||||
GCC_PREFIX_HEADER = "Source/MasterPassword-Prefix.pch";
|
||||
GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-ios~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-ios~/out/include\"",
|
||||
);
|
||||
INFOPLIST_FILE = "Source/iOS/MasterPassword-Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../lib/libsodium/build-ios~/out/lib",
|
||||
"$(PROJECT_DIR)/../lib/libjson-c/build-ios~/out/lib",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-ios~/out/lib\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-ios~/out/lib\"",
|
||||
);
|
||||
OTHER_CFLAGS = (
|
||||
"-DMPW_SODIUM=1",
|
||||
@@ -4712,13 +4704,18 @@
|
||||
GCC_C_LANGUAGE_STANDARD = c11;
|
||||
GCC_PREFIX_HEADER = "Source/MasterPassword-Prefix.pch";
|
||||
GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-ios~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-ios~/out/include\"",
|
||||
);
|
||||
INFOPLIST_FILE = "Source/iOS/MasterPassword-Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../lib/libsodium/build-ios~/out/lib",
|
||||
"$(PROJECT_DIR)/../lib/libjson-c/build-ios~/out/lib",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-ios~/out/lib\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-ios~/out/lib\"",
|
||||
);
|
||||
OTHER_CFLAGS = (
|
||||
"-DMPW_SODIUM=1",
|
||||
|
||||
@@ -1224,7 +1224,7 @@
|
||||
DA1C7ABC1F1A8F6E009A3551 /* mpw-tests.c */,
|
||||
);
|
||||
name = cli;
|
||||
path = "../../platform-independent/cli-c/cli";
|
||||
path = ../../cli/src;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DA2508F819513C1400AC23F1 /* Other Frameworks */ = {
|
||||
@@ -1820,7 +1820,7 @@
|
||||
DA6773C61A4746AF004F356A /* mpw-util.h */,
|
||||
);
|
||||
name = core;
|
||||
path = ../platform-independent/c/core/src;
|
||||
path = "../platform-independent/c/core/src";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DA89D4E51A51E53100AC64D7 /* Pearl-Cocoa */ = {
|
||||
@@ -2992,15 +2992,8 @@
|
||||
GCC_WARN_UNUSED_LABEL = YES;
|
||||
GCC_WARN_UNUSED_VALUE = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"\"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/$(PLATFORM_NAME)/include\"",
|
||||
"\"$(PROJECT_DIR)/External/libsodium/libsodium-osx/include\"",
|
||||
"\"$(PROJECT_DIR)/External/libjson-c/libjson-c-osx/include\"",
|
||||
"$(inherited)",
|
||||
);
|
||||
LD_DYLIB_INSTALL_NAME = "@rpath/$(EXECUTABLE_PATH)";
|
||||
LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = "$(inherit)";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
@@ -3028,12 +3021,17 @@
|
||||
);
|
||||
GCC_PREFIX_HEADER = "Source/MasterPassword-Prefix.pch";
|
||||
GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/include\"",
|
||||
);
|
||||
INFOPLIST_FILE = "Source/Mac/MasterPassword-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib",
|
||||
"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib\"",
|
||||
);
|
||||
OTHER_CFLAGS = (
|
||||
"-DMPW_SODIUM=1",
|
||||
@@ -3084,13 +3082,15 @@
|
||||
buildSettings = {
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"\"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/$(PLATFORM_NAME)/include\"",
|
||||
/usr/include/libxml2,
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/include\"",
|
||||
/usr/include/libxml2,
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib\"",
|
||||
);
|
||||
OTHER_CFLAGS = (
|
||||
"-DMPW_SODIUM=1",
|
||||
@@ -3103,10 +3103,15 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/include\"",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib",
|
||||
"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib\"",
|
||||
);
|
||||
OTHER_CFLAGS = (
|
||||
"-DMPW_SODIUM=1",
|
||||
@@ -3122,10 +3127,15 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/include\"",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib",
|
||||
"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib\"",
|
||||
);
|
||||
OTHER_CFLAGS = (
|
||||
"-DMPW_SODIUM=1",
|
||||
@@ -3141,10 +3151,15 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/include\"",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib",
|
||||
"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib\"",
|
||||
);
|
||||
OTHER_CFLAGS = (
|
||||
"-DMPW_SODIUM=1",
|
||||
@@ -3160,9 +3175,15 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/include\"",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib\"",
|
||||
);
|
||||
OTHER_CFLAGS = (
|
||||
"-DMPW_SODIUM=1",
|
||||
@@ -3176,9 +3197,15 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/include\"",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib\"",
|
||||
);
|
||||
OTHER_CFLAGS = (
|
||||
"-DMPW_SODIUM=1",
|
||||
@@ -3192,9 +3219,15 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/include\"",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib\"",
|
||||
);
|
||||
OTHER_CFLAGS = (
|
||||
"-DMPW_SODIUM=1",
|
||||
@@ -3348,15 +3381,8 @@
|
||||
GCC_WARN_UNUSED_LABEL = YES;
|
||||
GCC_WARN_UNUSED_VALUE = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"\"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/$(PLATFORM_NAME)/include\"",
|
||||
"\"$(PROJECT_DIR)/External/libsodium/libsodium-osx/include\"",
|
||||
"\"$(PROJECT_DIR)/External/libjson-c/libjson-c-osx/include\"",
|
||||
"$(inherited)",
|
||||
);
|
||||
LD_DYLIB_INSTALL_NAME = "@rpath/$(EXECUTABLE_PATH)";
|
||||
LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = "$(inherit)";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -3455,15 +3481,8 @@
|
||||
GCC_WARN_UNUSED_LABEL = YES;
|
||||
GCC_WARN_UNUSED_VALUE = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"\"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/$(PLATFORM_NAME)/include\"",
|
||||
"\"$(PROJECT_DIR)/External/libsodium/libsodium-osx/include\"",
|
||||
"\"$(PROJECT_DIR)/External/libjson-c/libjson-c-osx/include\"",
|
||||
"$(inherited)",
|
||||
);
|
||||
LD_DYLIB_INSTALL_NAME = "@rpath/$(EXECUTABLE_PATH)";
|
||||
LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = "$(inherit)";
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.11;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_LDFLAGS = "-ObjC";
|
||||
@@ -3491,12 +3510,17 @@
|
||||
);
|
||||
GCC_PREFIX_HEADER = "Source/MasterPassword-Prefix.pch";
|
||||
GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/include\"",
|
||||
);
|
||||
INFOPLIST_FILE = "Source/Mac/MasterPassword-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib",
|
||||
"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib\"",
|
||||
);
|
||||
OTHER_CFLAGS = (
|
||||
"-DMPW_SODIUM=1",
|
||||
@@ -3523,12 +3547,17 @@
|
||||
);
|
||||
GCC_PREFIX_HEADER = "Source/MasterPassword-Prefix.pch";
|
||||
GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/include\"",
|
||||
);
|
||||
INFOPLIST_FILE = "Source/Mac/MasterPassword-Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib",
|
||||
"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib\"",
|
||||
);
|
||||
OTHER_CFLAGS = (
|
||||
"-DMPW_SODIUM=1",
|
||||
@@ -3545,13 +3574,15 @@
|
||||
buildSettings = {
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"\"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/$(PLATFORM_NAME)/include\"",
|
||||
/usr/include/libxml2,
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/include\"",
|
||||
/usr/include/libxml2,
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib\"",
|
||||
);
|
||||
OTHER_CFLAGS = (
|
||||
"-DMPW_SODIUM=1",
|
||||
@@ -3565,13 +3596,15 @@
|
||||
buildSettings = {
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"\"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/$(PLATFORM_NAME)/include\"",
|
||||
/usr/include/libxml2,
|
||||
"$(inherited)",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/include\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/include\"",
|
||||
/usr/include/libxml2,
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib",
|
||||
"\"$(PROJECT_DIR)/../lib/libsodium/build-macos~/out/lib\"",
|
||||
"\"$(PROJECT_DIR)/../lib/libjson-c/build-macos~/out/lib\"",
|
||||
);
|
||||
OTHER_CFLAGS = (
|
||||
"-DMPW_SODIUM=1",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10117"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
|
||||
<capability name="box content view" minToolsVersion="7.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
<capability name="stacking Non-gravity area distributions on NSStackView" minToolsVersion="7.0" minSystemVersion="10.11"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@@ -27,7 +28,7 @@
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Master Password" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MPSitesWindow">
|
||||
<windowStyleMask key="styleMask" texturedBackground="YES" unifiedTitleAndToolbar="YES" fullSizeContentView="YES"/>
|
||||
<windowStyleMask key="styleMask" texturedBackground="YES" fullSizeContentView="YES"/>
|
||||
<windowCollectionBehavior key="collectionBehavior" moveToActiveSpace="YES" transient="YES" ignoresCycle="YES" fullScreenAuxiliary="YES"/>
|
||||
<rect key="contentRect" x="0.0" y="0.0" width="640" height="577"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
|
||||
@@ -35,7 +36,7 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="640" height="577"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<visualEffectView blendingMode="behindWindow" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="eRe-Ef-AZx">
|
||||
<visualEffectView blendingMode="behindWindow" material="appearanceBased" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="eRe-Ef-AZx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="640" height="577"/>
|
||||
</visualEffectView>
|
||||
<progressIndicator hidden="YES" wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="oSh-Ec-8Nf" userLabel="Progress Spinner">
|
||||
@@ -45,7 +46,7 @@
|
||||
<rect key="frame" x="20" y="383" width="600" height="150"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ond-dT-x5d" userLabel="Site Password Label">
|
||||
<rect key="frame" x="157" y="116" width="285" height="14"/>
|
||||
<rect key="frame" x="157" y="116" width="286" height="14"/>
|
||||
<shadow key="shadow" blurRadius="0.5">
|
||||
<size key="offset" width="0.0" height="1"/>
|
||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -73,7 +74,7 @@
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ia6-7b-dFr">
|
||||
<rect key="frame" x="115" y="68" width="370" height="14"/>
|
||||
<rect key="frame" x="127" y="68" width="347" height="14"/>
|
||||
<shadow key="shadow" blurRadius="0.5">
|
||||
<size key="offset" width="0.0" height="1"/>
|
||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -186,7 +187,7 @@
|
||||
</contentFilters>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="KeljXoleKowi9@" placeholderString="" id="WVV-EE-tkB">
|
||||
<font key="font" size="64" name="SourceCodePro-Regular"/>
|
||||
<color key="textColor" name="keyboardFocusIndicatorColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="textColor" name="selectedControlColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
@@ -223,13 +224,13 @@
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnReordering="NO" columnResizing="NO" multipleSelection="NO" emptySelection="NO" autosaveColumns="NO" rowHeight="33" rowSizeStyle="automatic" viewBased="YES" floatsGroupRows="NO" id="xvJ-5c-vDp" customClass="MPSitesTableView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="515" height="0.0"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="515" height="180"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<size key="intercellSpacing" width="3" height="2"/>
|
||||
<color key="backgroundColor" white="1" alpha="0.0" colorSpace="deviceWhite"/>
|
||||
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
|
||||
<tableColumns>
|
||||
<tableColumn editable="NO" width="512" minWidth="512" maxWidth="512" id="S71-gk-yF7">
|
||||
<tableColumn identifier="" editable="NO" width="512" minWidth="512" maxWidth="512" id="S71-gk-yF7">
|
||||
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left">
|
||||
<font key="font" metaFont="smallSystem"/>
|
||||
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -312,7 +313,7 @@
|
||||
</connections>
|
||||
</scrollView>
|
||||
<customView translatesAutoresizingMaskIntoConstraints="NO" id="nM8-O3-spM" customClass="MPGradientView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="640" height="212"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="640" height="214"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="color" keyPath="startingColor">
|
||||
<color key="value" red="0.7019608021" green="0.7019608021" blue="0.7019608021" alpha="0.0" colorSpace="calibratedRGB"/>
|
||||
@@ -570,7 +571,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
||||
</connections>
|
||||
</textField>
|
||||
<stackView distribution="fill" orientation="horizontal" alignment="bottom" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pHt-gg-ZNX">
|
||||
<rect key="frame" x="72" y="20" width="495" height="152"/>
|
||||
<rect key="frame" x="72" y="20" width="495" height="154"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1Qo-iG-CQt">
|
||||
<rect key="frame" x="0.0" y="-1" width="85" height="19"/>
|
||||
@@ -593,10 +594,10 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
||||
</connections>
|
||||
</button>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="centerX" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DT0-RU-3LT">
|
||||
<rect key="frame" x="93" y="0.0" width="177" height="152"/>
|
||||
<rect key="frame" x="93" y="0.0" width="177" height="154"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="uol-dE-I8H">
|
||||
<rect key="frame" x="77" y="138" width="23" height="14"/>
|
||||
<rect key="frame" x="77" y="140" width="23" height="14"/>
|
||||
<shadow key="shadow" blurRadius="0.5">
|
||||
<size key="offset" width="0.0" height="1"/>
|
||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -633,7 +634,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
||||
</connections>
|
||||
</textField>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="brI-fg-Kav">
|
||||
<rect key="frame" x="40" y="111" width="96" height="19"/>
|
||||
<rect key="frame" x="41" y="113" width="96" height="19"/>
|
||||
<shadow key="shadow">
|
||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||
</shadow>
|
||||
@@ -662,7 +663,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="R46-fx-n14">
|
||||
<rect key="frame" x="0.0" y="85" width="177" height="19"/>
|
||||
<rect key="frame" x="0.0" y="87" width="177" height="19"/>
|
||||
<shadow key="shadow">
|
||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||
</shadow>
|
||||
@@ -688,10 +689,10 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
||||
</connections>
|
||||
</button>
|
||||
<stackView distribution="fill" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Bgn-Ne-fQ7" userLabel="Version Container">
|
||||
<rect key="frame" x="70" y="56" width="36" height="22"/>
|
||||
<rect key="frame" x="71" y="57" width="36" height="23"/>
|
||||
<subviews>
|
||||
<stepper horizontalHuggingPriority="750" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mcq-qD-yte">
|
||||
<rect key="frame" x="-3" y="-3" width="19" height="27"/>
|
||||
<rect key="frame" x="-3" y="-2" width="19" height="27"/>
|
||||
<shadow key="shadow">
|
||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||
</shadow>
|
||||
@@ -701,7 +702,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
||||
</connections>
|
||||
</stepper>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gyg-Fh-yn7">
|
||||
<rect key="frame" x="19" y="3" width="19" height="19"/>
|
||||
<rect key="frame" x="19" y="4" width="19" height="19"/>
|
||||
<shadow key="shadow" blurRadius="0.5">
|
||||
<size key="offset" width="0.0" height="1"/>
|
||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -742,10 +743,10 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
||||
</connections>
|
||||
</stackView>
|
||||
<stackView distribution="fill" orientation="horizontal" alignment="centerY" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6II-KA-cNi" userLabel="Counter Container">
|
||||
<rect key="frame" x="74" y="26" width="28" height="22"/>
|
||||
<rect key="frame" x="75" y="26" width="28" height="23"/>
|
||||
<subviews>
|
||||
<stepper horizontalHuggingPriority="750" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="XgA-Vl-CKh" userLabel="Counter Stepper">
|
||||
<rect key="frame" x="-3" y="-3" width="19" height="27"/>
|
||||
<rect key="frame" x="-3" y="-2" width="19" height="27"/>
|
||||
<shadow key="shadow">
|
||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||
</shadow>
|
||||
@@ -755,7 +756,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
||||
</connections>
|
||||
</stepper>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="NvO-kt-eZ2" userLabel="Counter Field">
|
||||
<rect key="frame" x="19" y="1" width="11" height="19"/>
|
||||
<rect key="frame" x="19" y="2" width="11" height="19"/>
|
||||
<shadow key="shadow" blurRadius="0.5">
|
||||
<size key="offset" width="0.0" height="1"/>
|
||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -888,7 +889,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="luC-0j-BeV">
|
||||
<rect key="frame" x="134" y="50" width="103" height="14"/>
|
||||
<rect key="frame" x="135" y="51" width="103" height="14"/>
|
||||
<shadow key="shadow" blurRadius="0.5">
|
||||
<size key="offset" width="0.0" height="1"/>
|
||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -925,7 +926,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gjx-bt-fKM">
|
||||
<rect key="frame" x="133" y="80" width="100" height="14"/>
|
||||
<rect key="frame" x="134" y="82" width="100" height="14"/>
|
||||
<shadow key="shadow" blurRadius="0.5">
|
||||
<size key="offset" width="0.0" height="1"/>
|
||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -962,7 +963,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="dbM-ja-dKO" userLabel="Version Tip">
|
||||
<rect key="frame" x="87" y="106" width="332" height="14"/>
|
||||
<rect key="frame" x="88" y="108" width="332" height="14"/>
|
||||
<shadow key="shadow" blurRadius="0.5">
|
||||
<size key="offset" width="0.0" height="1"/>
|
||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -1170,7 +1171,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
||||
<binding destination="-2" name="contentArray" keyPath="sites" id="c96-Dv-HK1"/>
|
||||
</connections>
|
||||
</arrayController>
|
||||
<box autoresizesSubviews="NO" title="Choose a password type for apple.com:" borderType="line" id="bZe-7q-i6q">
|
||||
<box autoresizesSubviews="NO" borderType="line" title="Choose a password type for apple.com:" id="bZe-7q-i6q">
|
||||
<rect key="frame" x="0.0" y="0.0" width="416" height="296"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<view key="contentView" id="hAc-y9-IMT">
|
||||
@@ -1243,11 +1244,9 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
||||
<constraint firstItem="3fr-Fd-pxx" firstAttribute="top" secondItem="hAc-y9-IMT" secondAttribute="top" constant="8" id="xVT-HC-qsE"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<color key="borderColor" white="0.0" alpha="0.41999999999999998" colorSpace="calibratedWhite"/>
|
||||
<color key="fillColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
<point key="canvasLocation" x="333" y="125"/>
|
||||
</box>
|
||||
<box autoresizesSubviews="NO" title="Answer to security questions for apple.com:" borderType="line" id="hi3-SX-Td3">
|
||||
<box autoresizesSubviews="NO" borderType="line" title="Answer to security questions for apple.com:" id="hi3-SX-Td3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="416" height="180"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<view key="contentView" wantsLayer="YES" id="8Ep-zH-Nzv">
|
||||
@@ -1321,8 +1320,6 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
||||
<constraint firstAttribute="trailing" secondItem="12d-V9-LDB" secondAttribute="trailing" constant="12" id="Ysu-F5-ukt"/>
|
||||
<constraint firstItem="12d-V9-LDB" firstAttribute="leading" secondItem="hi3-SX-Td3" secondAttribute="leading" constant="12" id="oPv-4N-T9I"/>
|
||||
</constraints>
|
||||
<color key="borderColor" white="0.0" alpha="0.41999999999999998" colorSpace="calibratedWhite"/>
|
||||
<color key="fillColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
<point key="canvasLocation" x="333" y="491"/>
|
||||
</box>
|
||||
</objects>
|
||||
|
||||
@@ -219,7 +219,7 @@
|
||||
- (IBAction)sourceButton:(id)sender {
|
||||
|
||||
[[self dismissPopup].navigationController performSegueWithIdentifier:@"web" sender:
|
||||
[NSURL URLWithString:@"https://github.com/Lyndir/MasterPassword/"]];
|
||||
[NSURL URLWithString:@"https://gitlab.com/MasterPassword/MasterPassword/"]];
|
||||
}
|
||||
|
||||
- (IBAction)thanksButton:(id)sender {
|
||||
|
||||
@@ -9,71 +9,108 @@ plugins {
|
||||
description = 'Master Password Algorithm Implementation'
|
||||
|
||||
artifacts {
|
||||
'default' task( type: Zip, "archive" ) {
|
||||
'default' task( type: Zip, 'archive' ) {
|
||||
// TODO: exclude lib files that are produced by the build.
|
||||
from 'lib'
|
||||
|
||||
components.withType( ComponentWithRuntimeFile ) {
|
||||
if (isOptimized()) {
|
||||
from getRuntimeFile()
|
||||
into standardOperatingSystem( linkTask.get().targetPlatform.get() ) + '/' +
|
||||
standardArchitecture( linkTask.get().targetPlatform.get() )
|
||||
}
|
||||
if (optimized)
|
||||
from runtimeFile, {
|
||||
into standardOperatingSystem( targetPlatform ) + '/' + standardArchitecture( targetPlatform )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
library {
|
||||
baseName.set( "mpw" )
|
||||
linkage.set( [Linkage.STATIC, Linkage.SHARED] )
|
||||
baseName.set( 'mpw' )
|
||||
linkage.set( [Linkage.SHARED] )
|
||||
|
||||
// Reconfigure the toolchain from C++ to C.
|
||||
toolChains {
|
||||
withType( GccCompatibleToolChain ) {
|
||||
withType( VisualCpp ) {
|
||||
eachPlatform {
|
||||
cppCompiler.withArguments { addAll( ["-x", "c", "-std=c11", "-Werror", "-DMPW_SODIUM=1"] ) }
|
||||
cppCompiler.withArguments { addAll( ['/TC', '/MT', '/Ox', '/DMPW_SODIUM=1', '/DSODIUM_STATIC', '/DSODIUM_EXPORT='] ) }
|
||||
}
|
||||
}
|
||||
withType( GccCompatibleToolChain ) {
|
||||
eachPlatform {
|
||||
cppCompiler.withArguments { addAll( ['-x', 'c', '-O3', '-Werror', '-DMPW_SODIUM=1'] ) }
|
||||
}
|
||||
}
|
||||
}
|
||||
components.withType( CppComponent ) {
|
||||
cppSource.from fileTree( dir: "src", include: "**/*.c" )
|
||||
}
|
||||
|
||||
// Cross-compile for these host platforms.
|
||||
// TODO: Cross-compiling, blocked by: https://github.com/gradle/gradle-native/issues/169 - CppLibraryPlugin.java:163
|
||||
operatingSystems.set( [objects.named( OperatingSystemFamily, OperatingSystemFamily.WINDOWS ),
|
||||
objects.named( OperatingSystemFamily, OperatingSystemFamily.LINUX ),
|
||||
objects.named( OperatingSystemFamily, OperatingSystemFamily.MAC_OS )] )
|
||||
operatingSystems.set( [objects.named( OperatingSystemFamily, OperatingSystemFamily.LINUX ),
|
||||
objects.named( OperatingSystemFamily, OperatingSystemFamily.MAC_OS ),
|
||||
objects.named( OperatingSystemFamily, OperatingSystemFamily.WINDOWS )] )
|
||||
|
||||
binaries.configureEach {
|
||||
// Resolve a standard name for the platform.
|
||||
def platform = standardOperatingSystem( targetPlatform )
|
||||
components.withType( CppComponent ) {
|
||||
cppSource.from fileTree( 'src' )
|
||||
|
||||
project.dependencies {
|
||||
// Depend on JDK for JNI support.
|
||||
add( includePathConfiguration.name,
|
||||
files( new File( Jvm.current().javaHome, "include" ) ) { first().eachDir { from it } } )
|
||||
privateHeaders {
|
||||
// JDK for JNI support.
|
||||
from files( new File( Jvm.current().javaHome, 'include' ) ) { first().eachDir { from it } }
|
||||
}
|
||||
|
||||
// Depend on libsodium from `lib`; run `lib/bin/build_libsodium-${platform}` first.
|
||||
add( includePathConfiguration.name,
|
||||
files( "../../../lib/libsodium/build-${platform}~/out/include" ) )
|
||||
add( linkLibraries.name,
|
||||
fileTree( "../../../lib/libsodium/build-${platform}~/out/lib" ) )
|
||||
binaries.whenElementFinalized {
|
||||
project.dependencies {
|
||||
def system = standardOperatingSystem( targetPlatform )
|
||||
|
||||
// libsodium
|
||||
archive.dependsOn project.tasks.maybeCreate( "build_libsodium-${system}", Exec ).configure {
|
||||
commandLine 'bash', "$rootDir/../lib/bin/build_libsodium-${system}"
|
||||
privateHeaders.from "$rootDir/../lib/libsodium/build-${system}~/out/include"
|
||||
add( linkLibraries.name, fileTree( "$rootDir/../lib/libsodium/build-${system}~/out/lib" ) )
|
||||
}
|
||||
clean.dependsOn project.tasks.maybeCreate( "clean_libsodium-${system}", Exec ).configure {
|
||||
commandLine 'bash', "$rootDir/../lib/bin/build_libsodium-${system}", 'clean'
|
||||
}
|
||||
|
||||
// libjson-c
|
||||
/*archive.dependsOn project.tasks.maybeCreate( "build_libjson-c-${system}", Exec ).configure {
|
||||
commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-${system}"
|
||||
privateHeaders.from "$rootDir/../lib/libjson-c/build-${system}~/out/include"
|
||||
add( linkLibraries.name, fileTree( "$rootDir/../lib/libjson-c/build-${system}~/out/lib" ) )
|
||||
}
|
||||
clean.dependsOn project.tasks.maybeCreate( "clean_libjson-c-${system}", Exec ).configure {
|
||||
commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-${system}", 'clean'
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static String standardOperatingSystem(NativePlatform platform) {
|
||||
OperatingSystem os = platform.getOperatingSystem()
|
||||
if (os.isWindows()) {
|
||||
if (os.isWindows())
|
||||
return OperatingSystemFamily.WINDOWS
|
||||
} else if (os.isLinux()) {
|
||||
return OperatingSystemFamily.LINUX
|
||||
} else if (os.isMacOsX()) {
|
||||
return OperatingSystemFamily.MAC_OS
|
||||
}
|
||||
|
||||
return os.name.toLowerCase()
|
||||
else if (os.isMacOsX())
|
||||
return OperatingSystemFamily.MAC_OS
|
||||
|
||||
else if (os.isLinux())
|
||||
return OperatingSystemFamily.LINUX
|
||||
|
||||
// Other systems will need to use a Linux compatibility layer.
|
||||
return OperatingSystemFamily.LINUX
|
||||
}
|
||||
|
||||
static String standardArchitecture(NativePlatform platform) {
|
||||
Architecture arch = platform.getArchitecture()
|
||||
return arch.name.toLowerCase().replaceAll( "-", "_" )
|
||||
if (arch.isArm())
|
||||
return 'arm'
|
||||
|
||||
else if (arch.name.toLowerCase( Locale.ROOT ).startsWith( 'arm' ))
|
||||
return 'arm64'
|
||||
|
||||
else if (arch.isAmd64())
|
||||
return 'x86_64'
|
||||
|
||||
else if (arch.isI386())
|
||||
return 'x86'
|
||||
|
||||
// Other systems will need to be compatible with the x86 architecture.
|
||||
return 'x86'
|
||||
}
|
||||
|
||||
BIN
platform-independent/c/core/lib/linux/x86/libmpw.so
Executable file
BIN
platform-independent/c/core/lib/linux/x86_64/libmpw.so
Executable file
BIN
platform-independent/c/core/lib/windows/x86/mpw.dll
Normal file
BIN
platform-independent/c/core/lib/windows/x86_64/mpw.dll
Normal file
@@ -1,12 +1,13 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "mpw-jni.h"
|
||||
#include "java/com_lyndir_masterpassword_impl_MPAlgorithmV0.h"
|
||||
|
||||
#include "mpw-algorithm.h"
|
||||
#include "mpw-util.h"
|
||||
|
||||
// TODO: We may need to zero the jbytes safely.
|
||||
|
||||
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
JNIEnv* env;
|
||||
if ((*vm)->GetEnv( vm, (void **)&env, JNI_VERSION_1_6 ) != JNI_OK)
|
||||
return -1;
|
||||
|
||||
@@ -35,17 +35,21 @@ char *mpw_get_token(const char **in, const char *eol, char *delim) {
|
||||
return token;
|
||||
}
|
||||
|
||||
time_t mpw_mktime(
|
||||
const char *time) {
|
||||
time_t mpw_timegm(const char *time) {
|
||||
|
||||
// TODO: Support parsing timezone into tm_gmtoff
|
||||
// TODO: Support for parsing non-UTC time strings
|
||||
// Parse time as a UTC timestamp, into a tm.
|
||||
struct tm tm = { .tm_isdst = -1 };
|
||||
if (time && sscanf( time, "%4d-%2d-%2dT%2d:%2d:%2dZ",
|
||||
&tm.tm_year, &tm.tm_mon, &tm.tm_mday,
|
||||
&tm.tm_hour, &tm.tm_min, &tm.tm_sec ) == 6) {
|
||||
tm.tm_year -= 1900; // tm_year 0 = rfc3339 year 1900
|
||||
tm.tm_mon -= 1; // tm_mon 0 = rfc3339 month 1
|
||||
return mktime( &tm );
|
||||
|
||||
// mktime interprets tm as being local, we need to offset back to UTC (timegm/tm_gmtoff are non-standard).
|
||||
time_t local_time = mktime( &tm ), local_dst = tm.tm_isdst > 0? 3600: 0;
|
||||
time_t gmtoff = local_time + local_dst - mktime( gmtime( &local_time ) );
|
||||
return local_time + gmtoff;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
char *mpw_get_token(
|
||||
const char **in, const char *eol, char *delim);
|
||||
/** Convert an RFC 3339 time string into epoch time. */
|
||||
time_t mpw_mktime(
|
||||
time_t mpw_timegm(
|
||||
const char *time);
|
||||
|
||||
/// JSON parsing.
|
||||
|
||||
@@ -206,7 +206,7 @@ static bool mpw_marshal_write_flat(
|
||||
if (strftime( dateString, sizeof( dateString ), "%FT%TZ", gmtime( &site->lastUsed ) ))
|
||||
mpw_string_pushf( out, "%s %8ld %lu:%lu:%lu %25s\t%25s\t%s\n",
|
||||
dateString, (long)site->uses, (long)site->type, (long)site->algorithm, (long)site->counter,
|
||||
loginContent?: "", site->name, content?: "" );
|
||||
loginContent? loginContent: "", site->name, content? content: "" );
|
||||
mpw_free_strings( &content, &loginContent, NULL );
|
||||
}
|
||||
mpw_free( &masterKey, MPMasterKeySize );
|
||||
@@ -304,39 +304,43 @@ static bool mpw_marshal_write_json(
|
||||
if (strftime( dateString, sizeof( dateString ), "%FT%TZ", gmtime( &site->lastUsed ) ))
|
||||
json_object_object_add( json_site, "last_used", json_object_new_string( dateString ) );
|
||||
|
||||
json_object *json_site_questions = json_object_new_object();
|
||||
json_object_object_add( json_site, "questions", json_site_questions );
|
||||
for (size_t q = 0; q < site->questions_count; ++q) {
|
||||
MPMarshalledQuestion *question = &site->questions[q];
|
||||
if (!question->keyword)
|
||||
continue;
|
||||
if (site->questions_count) {
|
||||
json_object *json_site_questions = json_object_new_object();
|
||||
json_object_object_add( json_site, "questions", json_site_questions );
|
||||
for (size_t q = 0; q < site->questions_count; ++q) {
|
||||
MPMarshalledQuestion *question = &site->questions[q];
|
||||
if (!question->keyword)
|
||||
continue;
|
||||
|
||||
json_object *json_site_question = json_object_new_object();
|
||||
json_object_object_add( json_site_questions, question->keyword, json_site_question );
|
||||
json_object_object_add( json_site_question, "type", json_object_new_int( (int32_t)question->type ) );
|
||||
json_object *json_site_question = json_object_new_object();
|
||||
json_object_object_add( json_site_questions, question->keyword, json_site_question );
|
||||
json_object_object_add( json_site_question, "type", json_object_new_int( (int32_t)question->type ) );
|
||||
|
||||
if (!user->redacted) {
|
||||
// Clear Text
|
||||
const char *answerContent = mpw_siteResult( masterKey, site->name, MPCounterValueInitial,
|
||||
MPKeyPurposeRecovery, question->keyword, question->type, question->content, site->algorithm );
|
||||
json_object_object_add( json_site_question, "answer", json_object_new_string( answerContent ) );
|
||||
}
|
||||
else {
|
||||
// Redacted
|
||||
if (site->type & MPSiteFeatureExportContent && question->content && strlen( question->content ))
|
||||
json_object_object_add( json_site_question, "answer", json_object_new_string( question->content ) );
|
||||
if (!user->redacted) {
|
||||
// Clear Text
|
||||
const char *answerContent = mpw_siteResult( masterKey, site->name, MPCounterValueInitial,
|
||||
MPKeyPurposeRecovery, question->keyword, question->type, question->content, site->algorithm );
|
||||
json_object_object_add( json_site_question, "answer", json_object_new_string( answerContent ) );
|
||||
}
|
||||
else {
|
||||
// Redacted
|
||||
if (site->type & MPSiteFeatureExportContent && question->content && strlen( question->content ))
|
||||
json_object_object_add( json_site_question, "answer", json_object_new_string( question->content ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json_object *json_site_mpw = json_object_new_object();
|
||||
json_object_object_add( json_site, "_ext_mpw", json_site_mpw );
|
||||
if (site->url)
|
||||
json_object_object_add( json_site_mpw, "url", json_object_new_string( site->url ) );
|
||||
if (json_object_object_length( json_site_mpw ))
|
||||
json_object_object_add( json_site, "_ext_mpw", json_site_mpw );
|
||||
|
||||
mpw_free_strings( &content, &loginContent, NULL );
|
||||
}
|
||||
|
||||
mpw_string_pushf( out, "%s\n", json_object_to_json_string_ext( json_file, JSON_C_TO_STRING_PRETTY | JSON_C_TO_STRING_SPACED ) );
|
||||
mpw_string_pushf( out, "%s\n", json_object_to_json_string_ext( json_file,
|
||||
JSON_C_TO_STRING_PRETTY | JSON_C_TO_STRING_SPACED | JSON_C_TO_STRING_NOSLASHESCAPE ) );
|
||||
mpw_free( &masterKey, MPMasterKeySize );
|
||||
json_object_put( json_file );
|
||||
|
||||
@@ -403,7 +407,7 @@ static void mpw_marshal_read_flat_info(
|
||||
if (strcmp( headerName, "Passwords" ) == 0)
|
||||
info->redacted = strcmp( headerValue, "VISIBLE" ) != 0;
|
||||
if (strcmp( headerName, "Date" ) == 0)
|
||||
info->date = mpw_mktime( headerValue );
|
||||
info->date = mpw_timegm( headerValue );
|
||||
|
||||
mpw_free_strings( &headerName, &headerValue, NULL );
|
||||
continue;
|
||||
@@ -576,7 +580,7 @@ static MPMarshalledUser *mpw_marshal_read_flat(
|
||||
return NULL;
|
||||
}
|
||||
MPAlgorithmVersion siteAlgorithm = (MPAlgorithmVersion)value;
|
||||
time_t siteLastUsed = mpw_mktime( str_lastUsed );
|
||||
time_t siteLastUsed = mpw_timegm( str_lastUsed );
|
||||
if (!siteLastUsed) {
|
||||
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid site last used: %s: %s", siteName, str_lastUsed ) };
|
||||
return NULL;
|
||||
@@ -646,7 +650,7 @@ static void mpw_marshal_read_json_info(
|
||||
if (fileFormat < 1)
|
||||
return;
|
||||
info->redacted = mpw_get_json_boolean( json_file, "export.redacted", true );
|
||||
info->date = mpw_mktime( mpw_get_json_string( json_file, "export.date", NULL ) );
|
||||
info->date = mpw_timegm( mpw_get_json_string( json_file, "export.date", NULL ) );
|
||||
|
||||
// Section: "user"
|
||||
info->algorithm = (MPAlgorithmVersion)mpw_get_json_int( json_file, "user.algorithm", MPAlgorithmVersionCurrent );
|
||||
@@ -703,7 +707,7 @@ static MPMarshalledUser *mpw_marshal_read_json(
|
||||
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid user default type: %u", defaultType ) };
|
||||
return NULL;
|
||||
}
|
||||
time_t lastUsed = mpw_mktime( str_lastUsed );
|
||||
time_t lastUsed = mpw_timegm( str_lastUsed );
|
||||
if (!lastUsed) {
|
||||
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid user last used: %s", str_lastUsed ) };
|
||||
return NULL;
|
||||
@@ -756,7 +760,7 @@ static MPMarshalledUser *mpw_marshal_read_json(
|
||||
MPResultType siteLoginType = (MPResultType)mpw_get_json_int( json_site.val, "login_type", MPResultTypeTemplateName );
|
||||
unsigned int siteUses = (unsigned int)mpw_get_json_int( json_site.val, "uses", 0 );
|
||||
str_lastUsed = mpw_get_json_string( json_site.val, "last_used", NULL );
|
||||
time_t siteLastUsed = mpw_mktime( str_lastUsed );
|
||||
time_t siteLastUsed = mpw_timegm( str_lastUsed );
|
||||
if (!siteLastUsed) {
|
||||
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid site last used: %s: %s", siteName, str_lastUsed ) };
|
||||
return NULL;
|
||||
@@ -797,23 +801,25 @@ static MPMarshalledUser *mpw_marshal_read_json(
|
||||
site->loginContent = mpw_strdup( siteLoginName );
|
||||
}
|
||||
|
||||
json_object_iter json_site_question;
|
||||
json_object *json_site_questions = mpw_get_json_section( json_site.val, "questions" );
|
||||
json_object_object_foreachC( json_site_questions, json_site_question ) {
|
||||
MPMarshalledQuestion *question = mpw_marshal_question( site, json_site_question.key );
|
||||
const char *answerContent = mpw_get_json_string( json_site_question.val, "answer", NULL );
|
||||
question->type = (MPResultType)mpw_get_json_int( json_site_question.val, "type", MPResultTypeTemplatePhrase );
|
||||
if (json_site_questions) {
|
||||
json_object_iter json_site_question;
|
||||
json_object_object_foreachC( json_site_questions, json_site_question ) {
|
||||
MPMarshalledQuestion *question = mpw_marshal_question( site, json_site_question.key );
|
||||
const char *answerContent = mpw_get_json_string( json_site_question.val, "answer", NULL );
|
||||
question->type = (MPResultType)mpw_get_json_int( json_site_question.val, "type", MPResultTypeTemplatePhrase );
|
||||
|
||||
if (!user->redacted) {
|
||||
// Clear Text
|
||||
if (answerContent && strlen( answerContent ))
|
||||
question->content = mpw_siteState( masterKey, site->name, MPCounterValueInitial,
|
||||
MPKeyPurposeRecovery, question->keyword, question->type, answerContent, site->algorithm );
|
||||
}
|
||||
else {
|
||||
// Redacted
|
||||
if (answerContent && strlen( answerContent ))
|
||||
question->content = mpw_strdup( answerContent );
|
||||
if (!user->redacted) {
|
||||
// Clear Text
|
||||
if (answerContent && strlen( answerContent ))
|
||||
question->content = mpw_siteState( masterKey, site->name, MPCounterValueInitial,
|
||||
MPKeyPurposeRecovery, question->keyword, question->type, answerContent, site->algorithm );
|
||||
}
|
||||
else {
|
||||
// Redacted
|
||||
if (answerContent && strlen( answerContent ))
|
||||
question->content = mpw_strdup( answerContent );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -871,21 +877,14 @@ const MPMarshalFormat mpw_formatWithName(
|
||||
if (!formatName || !strlen( formatName ))
|
||||
return MPMarshalFormatNone;
|
||||
|
||||
// Lower-case to standardize it.
|
||||
size_t stdFormatNameSize = strlen( formatName );
|
||||
char stdFormatName[stdFormatNameSize + 1];
|
||||
for (size_t c = 0; c < stdFormatNameSize; ++c)
|
||||
stdFormatName[c] = (char)tolower( formatName[c] );
|
||||
stdFormatName[stdFormatNameSize] = '\0';
|
||||
|
||||
if (strncmp( mpw_nameForFormat( MPMarshalFormatNone ), stdFormatName, strlen( stdFormatName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForFormat( MPMarshalFormatNone ), formatName, strlen( formatName ) ) == 0)
|
||||
return MPMarshalFormatNone;
|
||||
if (strncmp( mpw_nameForFormat( MPMarshalFormatFlat ), stdFormatName, strlen( stdFormatName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForFormat( MPMarshalFormatFlat ), formatName, strlen( formatName ) ) == 0)
|
||||
return MPMarshalFormatFlat;
|
||||
if (strncmp( mpw_nameForFormat( MPMarshalFormatJSON ), stdFormatName, strlen( stdFormatName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForFormat( MPMarshalFormatJSON ), formatName, strlen( formatName ) ) == 0)
|
||||
return MPMarshalFormatJSON;
|
||||
|
||||
dbg( "Not a format name: %s", stdFormatName );
|
||||
dbg( "Not a format name: %s", formatName );
|
||||
return (MPMarshalFormat)ERR;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,38 +53,31 @@ const MPResultType mpw_typeWithName(const char *typeName) {
|
||||
return MPResultTypeDeriveKey;
|
||||
}
|
||||
|
||||
// Lower-case typeName to standardize it.
|
||||
size_t stdTypeNameSize = strlen( typeName );
|
||||
char stdTypeName[stdTypeNameSize + 1];
|
||||
for (size_t c = 0; c < stdTypeNameSize; ++c)
|
||||
stdTypeName[c] = (char)tolower( typeName[c] );
|
||||
stdTypeName[stdTypeNameSize] = '\0';
|
||||
|
||||
// Find what password type is represented by the type name.
|
||||
if (strncmp( mpw_nameForType( MPResultTypeTemplateMaximum ), stdTypeName, strlen( stdTypeName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForType( MPResultTypeTemplateMaximum ), typeName, strlen( typeName ) ) == 0)
|
||||
return MPResultTypeTemplateMaximum;
|
||||
if (strncmp( mpw_nameForType( MPResultTypeTemplateLong ), stdTypeName, strlen( stdTypeName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForType( MPResultTypeTemplateLong ), typeName, strlen( typeName ) ) == 0)
|
||||
return MPResultTypeTemplateLong;
|
||||
if (strncmp( mpw_nameForType( MPResultTypeTemplateMedium ), stdTypeName, strlen( stdTypeName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForType( MPResultTypeTemplateMedium ), typeName, strlen( typeName ) ) == 0)
|
||||
return MPResultTypeTemplateMedium;
|
||||
if (strncmp( mpw_nameForType( MPResultTypeTemplateBasic ), stdTypeName, strlen( stdTypeName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForType( MPResultTypeTemplateBasic ), typeName, strlen( typeName ) ) == 0)
|
||||
return MPResultTypeTemplateBasic;
|
||||
if (strncmp( mpw_nameForType( MPResultTypeTemplateShort ), stdTypeName, strlen( stdTypeName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForType( MPResultTypeTemplateShort ), typeName, strlen( typeName ) ) == 0)
|
||||
return MPResultTypeTemplateShort;
|
||||
if (strncmp( mpw_nameForType( MPResultTypeTemplatePIN ), stdTypeName, strlen( stdTypeName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForType( MPResultTypeTemplatePIN ), typeName, strlen( typeName ) ) == 0)
|
||||
return MPResultTypeTemplatePIN;
|
||||
if (strncmp( mpw_nameForType( MPResultTypeTemplateName ), stdTypeName, strlen( stdTypeName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForType( MPResultTypeTemplateName ), typeName, strlen( typeName ) ) == 0)
|
||||
return MPResultTypeTemplateName;
|
||||
if (strncmp( mpw_nameForType( MPResultTypeTemplatePhrase ), stdTypeName, strlen( stdTypeName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForType( MPResultTypeTemplatePhrase ), typeName, strlen( typeName ) ) == 0)
|
||||
return MPResultTypeTemplatePhrase;
|
||||
if (strncmp( mpw_nameForType( MPResultTypeStatefulPersonal ), stdTypeName, strlen( stdTypeName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForType( MPResultTypeStatefulPersonal ), typeName, strlen( typeName ) ) == 0)
|
||||
return MPResultTypeStatefulPersonal;
|
||||
if (strncmp( mpw_nameForType( MPResultTypeStatefulDevice ), stdTypeName, strlen( stdTypeName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForType( MPResultTypeStatefulDevice ), typeName, strlen( typeName ) ) == 0)
|
||||
return MPResultTypeStatefulDevice;
|
||||
if (strncmp( mpw_nameForType( MPResultTypeDeriveKey ), stdTypeName, strlen( stdTypeName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForType( MPResultTypeDeriveKey ), typeName, strlen( typeName ) ) == 0)
|
||||
return MPResultTypeDeriveKey;
|
||||
|
||||
dbg( "Not a generated type name: %s", stdTypeName );
|
||||
dbg( "Not a generated type name: %s", typeName );
|
||||
return (MPResultType)ERR;
|
||||
}
|
||||
|
||||
@@ -129,35 +122,35 @@ const char **mpw_templatesForType(MPResultType type, size_t *count) {
|
||||
|
||||
switch (type) {
|
||||
case MPResultTypeTemplateMaximum:
|
||||
return mpw_alloc_array( count, const char *,
|
||||
"anoxxxxxxxxxxxxxxxxx", "axxxxxxxxxxxxxxxxxno" );
|
||||
return mpw_strings( count,
|
||||
"anoxxxxxxxxxxxxxxxxx", "axxxxxxxxxxxxxxxxxno", NULL );
|
||||
case MPResultTypeTemplateLong:
|
||||
return mpw_alloc_array( count, const char *,
|
||||
return mpw_strings( count,
|
||||
"CvcvnoCvcvCvcv", "CvcvCvcvnoCvcv", "CvcvCvcvCvcvno",
|
||||
"CvccnoCvcvCvcv", "CvccCvcvnoCvcv", "CvccCvcvCvcvno",
|
||||
"CvcvnoCvccCvcv", "CvcvCvccnoCvcv", "CvcvCvccCvcvno",
|
||||
"CvcvnoCvcvCvcc", "CvcvCvcvnoCvcc", "CvcvCvcvCvccno",
|
||||
"CvccnoCvccCvcv", "CvccCvccnoCvcv", "CvccCvccCvcvno",
|
||||
"CvcvnoCvccCvcc", "CvcvCvccnoCvcc", "CvcvCvccCvccno",
|
||||
"CvccnoCvcvCvcc", "CvccCvcvnoCvcc", "CvccCvcvCvccno" );
|
||||
"CvccnoCvcvCvcc", "CvccCvcvnoCvcc", "CvccCvcvCvccno", NULL );
|
||||
case MPResultTypeTemplateMedium:
|
||||
return mpw_alloc_array( count, const char *,
|
||||
"CvcnoCvc", "CvcCvcno" );
|
||||
return mpw_strings( count,
|
||||
"CvcnoCvc", "CvcCvcno", NULL );
|
||||
case MPResultTypeTemplateShort:
|
||||
return mpw_alloc_array( count, const char *,
|
||||
"Cvcn" );
|
||||
return mpw_strings( count,
|
||||
"Cvcn", NULL );
|
||||
case MPResultTypeTemplateBasic:
|
||||
return mpw_alloc_array( count, const char *,
|
||||
"aaanaaan", "aannaaan", "aaannaaa" );
|
||||
return mpw_strings( count,
|
||||
"aaanaaan", "aannaaan", "aaannaaa", NULL );
|
||||
case MPResultTypeTemplatePIN:
|
||||
return mpw_alloc_array( count, const char *,
|
||||
"nnnn" );
|
||||
return mpw_strings( count,
|
||||
"nnnn", NULL );
|
||||
case MPResultTypeTemplateName:
|
||||
return mpw_alloc_array( count, const char *,
|
||||
"cvccvcvcv" );
|
||||
return mpw_strings( count,
|
||||
"cvccvcvcv", NULL );
|
||||
case MPResultTypeTemplatePhrase:
|
||||
return mpw_alloc_array( count, const char *,
|
||||
"cvcc cvc cvccvcv cvc", "cvc cvccvcvcv cvcv", "cv cvccv cvc cvcvccv" );
|
||||
return mpw_strings( count,
|
||||
"cvcc cvc cvccvcv cvc", "cvc cvccvcvcv cvcv", "cv cvccv cvc cvcvccv", NULL );
|
||||
default: {
|
||||
dbg( "Unknown generated type: %d", type );
|
||||
return NULL;
|
||||
@@ -177,21 +170,14 @@ const char *mpw_templateForType(MPResultType type, uint8_t templateIndex) {
|
||||
|
||||
const MPKeyPurpose mpw_purposeWithName(const char *purposeName) {
|
||||
|
||||
// Lower-case and trim optionally leading "generated" string from typeName to standardize it.
|
||||
size_t stdPurposeNameSize = strlen( purposeName );
|
||||
char stdPurposeName[stdPurposeNameSize + 1];
|
||||
for (size_t c = 0; c < stdPurposeNameSize; ++c)
|
||||
stdPurposeName[c] = (char)tolower( purposeName[c] );
|
||||
stdPurposeName[stdPurposeNameSize] = '\0';
|
||||
|
||||
if (strncmp( mpw_nameForPurpose( MPKeyPurposeAuthentication ), stdPurposeName, strlen( stdPurposeName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForPurpose( MPKeyPurposeAuthentication ), purposeName, strlen( purposeName ) ) == 0)
|
||||
return MPKeyPurposeAuthentication;
|
||||
if (strncmp( mpw_nameForPurpose( MPKeyPurposeIdentification ), stdPurposeName, strlen( stdPurposeName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForPurpose( MPKeyPurposeIdentification ), purposeName, strlen( purposeName ) ) == 0)
|
||||
return MPKeyPurposeIdentification;
|
||||
if (strncmp( mpw_nameForPurpose( MPKeyPurposeRecovery ), stdPurposeName, strlen( stdPurposeName ) ) == 0)
|
||||
if (mpw_strncasecmp( mpw_nameForPurpose( MPKeyPurposeRecovery ), purposeName, strlen( purposeName ) ) == 0)
|
||||
return MPKeyPurposeRecovery;
|
||||
|
||||
dbg( "Not a purpose name: %s", stdPurposeName );
|
||||
dbg( "Not a purpose name: %s", purposeName );
|
||||
return (MPKeyPurpose)ERR;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,27 @@ void mpw_uint64(const uint64_t number, uint8_t buf[8]) {
|
||||
buf[7] = (uint8_t)((number >> 0L) & UINT8_MAX);
|
||||
}
|
||||
|
||||
const char **mpw_strings(size_t *count, const char *strings, ...) {
|
||||
|
||||
va_list args;
|
||||
va_start( args, strings );
|
||||
const char **array = NULL;
|
||||
size_t size = 0;
|
||||
for (const char *string = strings; string; (string = va_arg( args, const char * ))) {
|
||||
size_t cursor = size / sizeof( *array );
|
||||
if (!mpw_realloc( &array, &size, sizeof( string ) )) {
|
||||
mpw_free( &array, size );
|
||||
*count = 0;
|
||||
return NULL;
|
||||
}
|
||||
array[cursor] = string;
|
||||
}
|
||||
va_end( args );
|
||||
|
||||
*count = size / sizeof( *array );
|
||||
return array;
|
||||
}
|
||||
|
||||
bool mpw_push_buf(uint8_t **buffer, size_t *bufferSize, const void *pushBuffer, const size_t pushSize) {
|
||||
|
||||
if (!buffer || !bufferSize || !pushBuffer || !pushSize)
|
||||
@@ -275,14 +296,15 @@ static uint8_t const *mpw_aes(bool encrypt, const uint8_t *key, const size_t key
|
||||
return NULL;
|
||||
|
||||
// IV = zero
|
||||
uint8_t iv[AES_BLOCKLEN];
|
||||
mpw_zero( iv, sizeof iv );
|
||||
static uint8_t *iv = NULL;
|
||||
if (!iv)
|
||||
iv = calloc( AES_BLOCKLEN, sizeof( uint8_t ) );
|
||||
|
||||
// Add PKCS#7 padding
|
||||
uint32_t aesSize = ((uint32_t)*bufSize + AES_BLOCKLEN - 1) & -AES_BLOCKLEN; // round up to block size.
|
||||
if (encrypt && !(*bufSize % AES_BLOCKLEN)) // add pad block if plain text fits block size.
|
||||
encrypt += AES_BLOCKLEN;
|
||||
uint8_t aesBuf[aesSize];
|
||||
uint8_t *aesBuf = malloc( aesSize );
|
||||
memcpy( aesBuf, buf, *bufSize );
|
||||
memset( aesBuf + *bufSize, aesSize - *bufSize, aesSize - *bufSize );
|
||||
uint8_t *resultBuf = malloc( aesSize );
|
||||
@@ -291,8 +313,7 @@ static uint8_t const *mpw_aes(bool encrypt, const uint8_t *key, const size_t key
|
||||
AES_CBC_encrypt_buffer( resultBuf, aesBuf, aesSize, key, iv );
|
||||
else
|
||||
AES_CBC_decrypt_buffer( resultBuf, aesBuf, aesSize, key, iv );
|
||||
mpw_zero( aesBuf, aesSize );
|
||||
mpw_zero( iv, AES_BLOCKLEN );
|
||||
mpw_free( &aesBuf, aesSize );
|
||||
|
||||
// Truncate PKCS#7 padding
|
||||
if (encrypt)
|
||||
@@ -496,3 +517,12 @@ char *mpw_strndup(const char *src, size_t max) {
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
int mpw_strncasecmp(const char *s1, const char *s2, size_t max) {
|
||||
|
||||
int cmp = 0;
|
||||
for (; !cmp && max-- > 0 && s1 && s2; ++s1, ++s2)
|
||||
cmp = tolower( *(unsigned char *)s1 ) - tolower( *(unsigned char *)s2 );
|
||||
|
||||
return cmp;
|
||||
}
|
||||
|
||||
@@ -33,36 +33,36 @@ extern int mpw_verbosity;
|
||||
#endif
|
||||
|
||||
#ifndef mpw_log
|
||||
#define mpw_log(level, ...) ({ \
|
||||
#define mpw_log(level, format, ...) do { \
|
||||
if (mpw_verbosity >= level) { \
|
||||
mpw_log_do( level, ##__VA_ARGS__ ); \
|
||||
}; })
|
||||
mpw_log_do( level, format, ##__VA_ARGS__ ); \
|
||||
}; } while (0)
|
||||
#endif
|
||||
|
||||
#ifndef trc
|
||||
/** Logging internal state. */
|
||||
#define trc_level 3
|
||||
#define trc(...) mpw_log( trc_level, ##__VA_ARGS__ )
|
||||
#define trc(format, ...) mpw_log( trc_level, format, ##__VA_ARGS__ )
|
||||
|
||||
/** Logging state and events interesting when investigating issues. */
|
||||
#define dbg_level 2
|
||||
#define dbg(...) mpw_log( dbg_level, ##__VA_ARGS__ )
|
||||
#define dbg(format, ...) mpw_log( dbg_level, format, ##__VA_ARGS__ )
|
||||
|
||||
/** User messages. */
|
||||
#define inf_level 1
|
||||
#define inf(...) mpw_log( inf_level, ##__VA_ARGS__ )
|
||||
#define inf(format, ...) mpw_log( inf_level, format, ##__VA_ARGS__ )
|
||||
|
||||
/** Recoverable issues and user suggestions. */
|
||||
#define wrn_level 0
|
||||
#define wrn(...) mpw_log( wrn_level, ##__VA_ARGS__ )
|
||||
#define wrn(format, ...) mpw_log( wrn_level, format, ##__VA_ARGS__ )
|
||||
|
||||
/** Unrecoverable issues. */
|
||||
#define err_level -1
|
||||
#define err(...) mpw_log( err_level, ##__VA_ARGS__ )
|
||||
#define err(format, ...) mpw_log( err_level, format, ##__VA_ARGS__ )
|
||||
|
||||
/** Issues that lead to abortion. */
|
||||
#define ftl_level -2
|
||||
#define ftl(...) mpw_log( ftl_level, ##__VA_ARGS__ )
|
||||
#define ftl(format, ...) mpw_log( ftl_level, format, ##__VA_ARGS__ )
|
||||
#endif
|
||||
|
||||
#ifndef min
|
||||
@@ -98,14 +98,8 @@ void mpw_uint32(const uint32_t number, uint8_t buf[4]);
|
||||
void mpw_uint64(const uint64_t number, uint8_t buf[8]);
|
||||
|
||||
/** Allocate a new array of _type, assign its element count to _count if not NULL and populate it with the varargs. */
|
||||
#define mpw_alloc_array(_count, _type, ...) ({ \
|
||||
_type stackElements[] = { __VA_ARGS__ }; \
|
||||
if (_count) \
|
||||
*_count = sizeof( stackElements ) / sizeof( _type ); \
|
||||
_type *allocElements = malloc( sizeof( stackElements ) ); \
|
||||
memcpy( allocElements, stackElements, sizeof( stackElements ) ); \
|
||||
allocElements; \
|
||||
})
|
||||
const char **mpw_strings(
|
||||
size_t *count, const char *strings, ...);
|
||||
|
||||
/** Push a buffer onto a buffer. reallocs the given buffer and appends the given buffer. */
|
||||
bool mpw_push_buf(
|
||||
@@ -124,9 +118,9 @@ bool mpw_push_int(
|
||||
/** Reallocate the given buffer from the given size by adding the delta size.
|
||||
* On success, the buffer size pointer will be updated to the buffer's new size
|
||||
* and the buffer pointer may be updated to a new memory address.
|
||||
* On failure, the buffer and pointers will remain unaffected.
|
||||
* On failure, the pointers will remain unaffected.
|
||||
* @param buffer A pointer to the buffer to reallocate.
|
||||
* @param bufferSize A pointer to the buffer's actual size.
|
||||
* @param bufferSize A pointer to the buffer's current size.
|
||||
* @param deltaSize The amount to increase the buffer's size by.
|
||||
* @return true if successful, false if reallocation failed.
|
||||
*/
|
||||
@@ -150,6 +144,20 @@ bool __mpw_free_string(
|
||||
({ __typeof__(strings) _s = strings; const char *__s = *_s; (void)__s; __mpw_free_strings( (char **)_s, __VA_ARGS__ ); })
|
||||
bool __mpw_free_strings(
|
||||
char **strings, ...);
|
||||
#ifdef _MSC_VER
|
||||
#undef mpw_realloc
|
||||
#define mpw_realloc(buffer, bufferSize, deltaSize) \
|
||||
__mpw_realloc( (const void **)buffer, bufferSize, deltaSize )
|
||||
#undef mpw_free
|
||||
#define mpw_free(buffer, bufferSize) \
|
||||
__mpw_free( (void **)buffer, bufferSize )
|
||||
#undef mpw_free_string
|
||||
#define mpw_free_string(string) \
|
||||
__mpw_free_string( (char **)string )
|
||||
#undef mpw_free_strings
|
||||
#define mpw_free_strings(strings, ...) \
|
||||
__mpw_free_strings( (char **)strings, __VA_ARGS__ )
|
||||
#endif
|
||||
|
||||
//// Cryptographic functions.
|
||||
|
||||
@@ -207,5 +215,7 @@ const size_t mpw_utf8_strlen(const char *utf8String);
|
||||
char *mpw_strdup(const char *src);
|
||||
/** Drop-in for POSIX strndup(3). */
|
||||
char *mpw_strndup(const char *src, size_t max);
|
||||
/** Drop-in for POSIX strncasecmp(3). */
|
||||
int mpw_strncasecmp(const char *s1, const char *s2, size_t max);
|
||||
|
||||
#endif // _MPW_UTIL_H
|
||||
|
||||
@@ -4,6 +4,11 @@ plugins {
|
||||
|
||||
description = 'Master Password Algorithm Implementation'
|
||||
|
||||
tasks.withType( JavaCompile ) {
|
||||
// Native headers
|
||||
options.compilerArgs += ["-h", new File( new File( project.project( ':masterpassword-core' ).projectDir, 'src' ), 'java' ).absolutePath]
|
||||
}
|
||||
|
||||
configurations {
|
||||
lib
|
||||
}
|
||||
@@ -20,20 +25,11 @@ dependencies {
|
||||
}
|
||||
|
||||
processResources {
|
||||
dependsOn task( type: Sync, 'copyResources', {
|
||||
dependsOn task( type: Sync, 'processResources-lib', {
|
||||
into new File( processResources.outputs.files.singleFile, "lib" )
|
||||
|
||||
dependsOn configurations.lib {
|
||||
files.each { libFile ->
|
||||
from zipTree( libFile )
|
||||
into new File( processResources.outputs.files.singleFile, "lib" )
|
||||
}
|
||||
files.each { libFile -> from( zipTree( libFile ) ) }
|
||||
}
|
||||
} )
|
||||
}
|
||||
|
||||
compileJava {
|
||||
doLast {
|
||||
ant.javah( class: 'com.lyndir.masterpassword.impl.MPAlgorithmV0',
|
||||
outputFile: '../../c/core/src/mpw-jni.h',
|
||||
classpath: files( sourceSets.main.compileClasspath, sourceSets.main.output ).asPath )
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.lyndir.lhunath.opal.system.MessageDigests;
|
||||
import com.lyndir.masterpassword.impl.*;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.Charset;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
@@ -88,46 +89,55 @@ public abstract class MPAlgorithm {
|
||||
/**
|
||||
* The linear version identifier of this algorithm's implementation.
|
||||
*/
|
||||
@Nonnull
|
||||
public abstract Version version();
|
||||
|
||||
/**
|
||||
* mpw: defaults: initial counter value.
|
||||
*/
|
||||
@Nonnull
|
||||
public abstract UnsignedInteger mpw_default_counter();
|
||||
|
||||
/**
|
||||
* mpw: defaults: password result type.
|
||||
*/
|
||||
@Nonnull
|
||||
public abstract MPResultType mpw_default_result_type();
|
||||
|
||||
/**
|
||||
* mpw: defaults: login result type.
|
||||
*/
|
||||
@Nonnull
|
||||
public abstract MPResultType mpw_default_login_type();
|
||||
|
||||
/**
|
||||
* mpw: defaults: answer result type.
|
||||
*/
|
||||
@Nonnull
|
||||
public abstract MPResultType mpw_default_answer_type();
|
||||
|
||||
/**
|
||||
* mpw: Input character encoding.
|
||||
*/
|
||||
@Nonnull
|
||||
public abstract Charset mpw_charset();
|
||||
|
||||
/**
|
||||
* mpw: Platform-agnostic byte order.
|
||||
*/
|
||||
@Nonnull
|
||||
public abstract ByteOrder mpw_byteOrder();
|
||||
|
||||
/**
|
||||
* mpw: Key ID hash.
|
||||
*/
|
||||
@Nonnull
|
||||
public abstract MessageDigests mpw_hash();
|
||||
|
||||
/**
|
||||
* mpw: Site digest.
|
||||
*/
|
||||
@Nonnull
|
||||
public abstract MessageAuthenticationDigests mpw_digest();
|
||||
|
||||
/**
|
||||
@@ -167,12 +177,16 @@ public abstract class MPAlgorithm {
|
||||
|
||||
// Utilities
|
||||
|
||||
@Nonnull
|
||||
protected abstract byte[] toBytes(int number);
|
||||
|
||||
@Nonnull
|
||||
protected abstract byte[] toBytes(UnsignedInteger number);
|
||||
|
||||
@Nonnull
|
||||
protected abstract byte[] toBytes(char[] characters);
|
||||
|
||||
@Nonnull
|
||||
protected abstract byte[] toID(byte[] bytes);
|
||||
|
||||
@Override
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import java.nio.*;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
/**
|
||||
@@ -83,6 +84,10 @@ public class MPIdenticon {
|
||||
return text;
|
||||
}
|
||||
|
||||
public String getHTML() {
|
||||
return strf( "<span style='color: %s'>%s</span>", color.getCSS(), text );
|
||||
}
|
||||
|
||||
public Color getColor() {
|
||||
return color;
|
||||
}
|
||||
@@ -94,6 +99,15 @@ public class MPIdenticon {
|
||||
BLUE,
|
||||
MAGENTA,
|
||||
CYAN,
|
||||
MONO
|
||||
MONO {
|
||||
@Override
|
||||
public String getCSS() {
|
||||
return "inherit";
|
||||
}
|
||||
};
|
||||
|
||||
public String getCSS() {
|
||||
return name().toLowerCase( Locale.ROOT );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ public class MPMasterKey {
|
||||
|
||||
/**
|
||||
* @param masterPassword The characters of the user's master password.
|
||||
* <b>Note: this method destroys the contents of the array.</b>
|
||||
*
|
||||
* @apiNote This method destroys the contents of the {@code masterPassword} array.
|
||||
*/
|
||||
@SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
|
||||
public MPMasterKey(final String fullName, final char[] masterPassword) {
|
||||
@@ -65,6 +66,7 @@ public class MPMasterKey {
|
||||
*
|
||||
* @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object.
|
||||
*/
|
||||
@Nonnull
|
||||
public byte[] getKeyID(final MPAlgorithm algorithm)
|
||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||
|
||||
@@ -82,11 +84,16 @@ public class MPMasterKey {
|
||||
Arrays.fill( masterPassword, (char) 0 );
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return !invalidated;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private byte[] masterKey(final MPAlgorithm algorithm)
|
||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||
Preconditions.checkArgument( masterPassword.length > 0 );
|
||||
|
||||
if (invalidated)
|
||||
if (!isValid())
|
||||
throw new MPKeyUnavailableException( "Master key was invalidated." );
|
||||
|
||||
byte[] masterKey = keyByVersion.get( algorithm.version() );
|
||||
@@ -104,6 +111,7 @@ public class MPMasterKey {
|
||||
return masterKey;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private byte[] siteKey(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter,
|
||||
final MPKeyPurpose keyPurpose, @Nullable final String keyContext)
|
||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||
@@ -136,13 +144,19 @@ public class MPMasterKey {
|
||||
* In the case of {@link MPResultTypeClass#Stateful} types, the result of
|
||||
* {@link #siteState(String, MPAlgorithm, UnsignedInteger, MPKeyPurpose, String, MPResultType, String)}.
|
||||
*
|
||||
* @return {@code null} if the result type is missing a required parameter.
|
||||
*
|
||||
* @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object.
|
||||
*/
|
||||
@Nullable
|
||||
public String siteResult(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter,
|
||||
final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
|
||||
final MPResultType resultType, @Nullable final String resultParam)
|
||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||
|
||||
if ((resultType.getTypeClass() == MPResultTypeClass.Stateful) && (resultParam == null))
|
||||
return null;
|
||||
|
||||
byte[] masterKey = masterKey( algorithm );
|
||||
byte[] siteKey = siteKey( siteName, algorithm, siteCounter, keyPurpose, keyContext );
|
||||
|
||||
@@ -171,6 +185,7 @@ public class MPMasterKey {
|
||||
*
|
||||
* @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object.
|
||||
*/
|
||||
@Nonnull
|
||||
public String siteState(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter,
|
||||
final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
|
||||
final MPResultType resultType, final String resultParam)
|
||||
|
||||
@@ -41,7 +41,7 @@ public enum MPResultType {
|
||||
/**
|
||||
* 16: pg^VMAUBk5x3p%HP%i4=
|
||||
*/
|
||||
GeneratedMaximum( "maximum", "20 characters, contains symbols.", //
|
||||
GeneratedMaximum( "maximum", "Maximum Security", "pg^VMAUBk5x3p%HP%i4=", "20 characters, contains symbols", //
|
||||
ImmutableList.of( new MPTemplate( "anoxxxxxxxxxxxxxxxxx" ),
|
||||
new MPTemplate( "axxxxxxxxxxxxxxxxxno" ) ), //
|
||||
MPResultTypeClass.Template, 0x0 ),
|
||||
@@ -49,7 +49,7 @@ public enum MPResultType {
|
||||
/**
|
||||
* 17: BiroYena8:Kixa
|
||||
*/
|
||||
GeneratedLong( "long", "Copy-friendly, 14 characters, contains symbols.", //
|
||||
GeneratedLong( "long", "Long Password", "BiroYena8:Kixa", "Copy-friendly, 14 characters, contains symbols", //
|
||||
ImmutableList.of( new MPTemplate( "CvcvnoCvcvCvcv" ), new MPTemplate( "CvcvCvcvnoCvcv" ),
|
||||
new MPTemplate( "CvcvCvcvCvcvno" ), new MPTemplate( "CvccnoCvcvCvcv" ),
|
||||
new MPTemplate( "CvccCvcvnoCvcv" ), new MPTemplate( "CvccCvcvCvcvno" ),
|
||||
@@ -66,7 +66,7 @@ public enum MPResultType {
|
||||
/**
|
||||
* 18: BirSuj0-
|
||||
*/
|
||||
GeneratedMedium( "medium", "Copy-friendly, 8 characters, contains symbols.", //
|
||||
GeneratedMedium( "medium", "Medium Password", "BirSuj0-", "Copy-friendly, 8 characters, contains symbols", //
|
||||
ImmutableList.of( new MPTemplate( "CvcnoCvc" ),
|
||||
new MPTemplate( "CvcCvcno" ) ), //
|
||||
MPResultTypeClass.Template, 0x2 ),
|
||||
@@ -74,14 +74,14 @@ public enum MPResultType {
|
||||
/**
|
||||
* 19: Bir8
|
||||
*/
|
||||
GeneratedShort( "short", "Copy-friendly, 4 characters, no symbols.", //
|
||||
GeneratedShort( "short", "Short Password", "Bir8", "Copy-friendly, 4 characters, no symbols", //
|
||||
ImmutableList.of( new MPTemplate( "Cvcn" ) ), //
|
||||
MPResultTypeClass.Template, 0x3 ),
|
||||
|
||||
/**
|
||||
* 20: pO98MoD0
|
||||
*/
|
||||
GeneratedBasic( "basic", "8 characters, no symbols.", //
|
||||
GeneratedBasic( "basic", "Basic Password", "pO98MoD0", "8 characters, no symbols", //
|
||||
ImmutableList.of( new MPTemplate( "aaanaaan" ),
|
||||
new MPTemplate( "aannaaan" ),
|
||||
new MPTemplate( "aaannaaa" ) ), //
|
||||
@@ -90,60 +90,67 @@ public enum MPResultType {
|
||||
/**
|
||||
* 21: 2798
|
||||
*/
|
||||
GeneratedPIN( "pin", "4 numbers.", //
|
||||
GeneratedPIN( "pin", "PIN Code", "2798", "4 numbers", //
|
||||
ImmutableList.of( new MPTemplate( "nnnn" ) ), //
|
||||
MPResultTypeClass.Template, 0x5 ),
|
||||
|
||||
/**
|
||||
* 30: birsujano
|
||||
*/
|
||||
GeneratedName( "name", "9 letter name.", //
|
||||
GeneratedName( "name", "Name", "birsujano", "9 letter name", //
|
||||
ImmutableList.of( new MPTemplate( "cvccvcvcv" ) ), //
|
||||
MPResultTypeClass.Template, 0xE ),
|
||||
|
||||
/**
|
||||
* 31: bir yennoquce fefi
|
||||
*/
|
||||
GeneratedPhrase( "phrase", "20 character sentence.", //
|
||||
GeneratedPhrase( "phrase", "Phrase", "bir yennoquce fefi", "20 character sentence", //
|
||||
ImmutableList.of( new MPTemplate( "cvcc cvc cvccvcv cvc" ),
|
||||
new MPTemplate( "cvc cvccvcvcv cvcv" ),
|
||||
new MPTemplate( "cv cvccv cvc cvcvccv" ) ), //
|
||||
MPResultTypeClass.Template, 0xF ),
|
||||
|
||||
/**
|
||||
* 1056: Custom saved password.
|
||||
* 1056: Custom saved value.
|
||||
*/
|
||||
StoredPersonal( "personal", "AES-encrypted, exportable.", //
|
||||
StoredPersonal( "personal", "Saved", null, "AES-encrypted, exportable", //
|
||||
ImmutableList.<MPTemplate>of(), //
|
||||
MPResultTypeClass.Stateful, 0x0, MPSiteFeature.ExportContent ),
|
||||
|
||||
/**
|
||||
* 2081: Custom saved password that should not be exported from the device.
|
||||
* 2081: Custom saved value that should not be exported from the device.
|
||||
*/
|
||||
StoredDevicePrivate( "device", "AES-encrypted, not exported.", //
|
||||
StoredDevicePrivate( "device", "Private", null, "AES-encrypted, not exported", //
|
||||
ImmutableList.<MPTemplate>of(), //
|
||||
MPResultTypeClass.Stateful, 0x1, MPSiteFeature.DevicePrivate ),
|
||||
|
||||
/**
|
||||
* 4160: Derive a unique binary key.
|
||||
*/
|
||||
DeriveKey( "key", "Encryption key.", //
|
||||
DeriveKey( "key", "Binary Key", null, "Encryption key", //
|
||||
ImmutableList.<MPTemplate>of(), //
|
||||
MPResultTypeClass.Derive, 0x0, MPSiteFeature.Alternative );
|
||||
|
||||
static final Logger logger = Logger.get( MPResultType.class );
|
||||
|
||||
private final String shortName;
|
||||
private final String longName;
|
||||
|
||||
@Nullable
|
||||
private final String sample;
|
||||
private final String description;
|
||||
private final List<MPTemplate> templates;
|
||||
private final MPResultTypeClass typeClass;
|
||||
private final int typeIndex;
|
||||
private final ImmutableSet<MPSiteFeature> typeFeatures;
|
||||
|
||||
MPResultType(final String shortName, final String description, final List<MPTemplate> templates,
|
||||
MPResultType(final String shortName, final String longName, @Nullable final String sample, final String description,
|
||||
final List<MPTemplate> templates,
|
||||
final MPResultTypeClass typeClass, final int typeIndex, final MPSiteFeature... typeFeatures) {
|
||||
|
||||
this.shortName = shortName;
|
||||
this.longName = longName;
|
||||
this.sample = sample;
|
||||
this.description = description;
|
||||
this.templates = templates;
|
||||
this.typeClass = typeClass;
|
||||
@@ -160,6 +167,15 @@ public enum MPResultType {
|
||||
return shortName;
|
||||
}
|
||||
|
||||
public String getLongName() {
|
||||
return longName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSample() {
|
||||
return sample;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
|
||||
return description;
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.lyndir.masterpassword.*;
|
||||
import java.nio.*;
|
||||
import java.nio.charset.*;
|
||||
import java.util.Arrays;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
@@ -45,8 +46,6 @@ public class MPAlgorithmV0 extends MPAlgorithm {
|
||||
Native.load( MPAlgorithmV0.class, "mpw" );
|
||||
}
|
||||
|
||||
public final Version version = MPAlgorithm.Version.V0;
|
||||
|
||||
protected final Logger logger = Logger.get( getClass() );
|
||||
|
||||
@Nullable
|
||||
@@ -124,46 +123,55 @@ public class MPAlgorithmV0 extends MPAlgorithm {
|
||||
|
||||
// Configuration
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Version version() {
|
||||
return MPAlgorithm.Version.V0;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public UnsignedInteger mpw_default_counter() {
|
||||
return UnsignedInteger.ONE;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public MPResultType mpw_default_result_type() {
|
||||
return MPResultType.GeneratedLong;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public MPResultType mpw_default_login_type() {
|
||||
return MPResultType.GeneratedName;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public MPResultType mpw_default_answer_type() {
|
||||
return MPResultType.GeneratedPhrase;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Charset mpw_charset() {
|
||||
return Charsets.UTF_8;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ByteOrder mpw_byteOrder() {
|
||||
return ByteOrder.BIG_ENDIAN;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public MessageDigests mpw_hash() {
|
||||
return MessageDigests.SHA256;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public MessageAuthenticationDigests mpw_digest() {
|
||||
return MessageAuthenticationDigests.HmacSHA256;
|
||||
@@ -213,16 +221,19 @@ public class MPAlgorithmV0 extends MPAlgorithm {
|
||||
|
||||
// Utilities
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public byte[] toBytes(final int number) {
|
||||
return ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( mpw_byteOrder() ).putInt( number ).array();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public byte[] toBytes(final UnsignedInteger number) {
|
||||
return ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( mpw_byteOrder() ).putInt( number.intValue() ).array();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public byte[] toBytes(final char[] characters) {
|
||||
ByteBuffer byteBuffer = mpw_charset().encode( CharBuffer.wrap( characters ) );
|
||||
@@ -234,6 +245,7 @@ public class MPAlgorithmV0 extends MPAlgorithm {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public byte[] toID(final byte[] bytes) {
|
||||
return mpw_hash().of( bytes );
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package com.lyndir.masterpassword.impl;
|
||||
|
||||
import com.lyndir.masterpassword.*;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
|
||||
/**
|
||||
@@ -29,6 +30,7 @@ public class MPAlgorithmV1 extends MPAlgorithmV0 {
|
||||
|
||||
// Configuration
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Version version() {
|
||||
return MPAlgorithm.Version.V1;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package com.lyndir.masterpassword.impl;
|
||||
|
||||
import com.lyndir.masterpassword.MPAlgorithm;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
|
||||
/**
|
||||
@@ -29,6 +30,7 @@ public class MPAlgorithmV2 extends MPAlgorithmV1 {
|
||||
|
||||
// Configuration
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Version version() {
|
||||
return MPAlgorithm.Version.V2;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package com.lyndir.masterpassword.impl;
|
||||
|
||||
import com.lyndir.masterpassword.MPAlgorithm;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
|
||||
/**
|
||||
@@ -29,6 +30,7 @@ public class MPAlgorithmV3 extends MPAlgorithmV2 {
|
||||
|
||||
// Configuration
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Version version() {
|
||||
return MPAlgorithm.Version.V3;
|
||||
|
||||
@@ -19,11 +19,15 @@
|
||||
package com.lyndir.masterpassword.impl;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import java.io.*;
|
||||
import java.util.Locale;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
@@ -39,54 +43,161 @@ public final class Native {
|
||||
private static final String NATIVES_PATH = "lib";
|
||||
|
||||
@SuppressWarnings({ "HardcodedFileSeparator", "LoadLibraryWithNonConstantString" })
|
||||
public static void load(final Class<?> context, final String name) {
|
||||
public static boolean load(final Class<?> context, final String name) {
|
||||
|
||||
// Try to load the library using the native system.
|
||||
try {
|
||||
System.loadLibrary( name );
|
||||
return;
|
||||
} catch (@SuppressWarnings("ErrorNotRethrown") final UnsatisfiedLinkError ignored) {
|
||||
return true;
|
||||
}
|
||||
catch (@SuppressWarnings("ErrorNotRethrown") final UnsatisfiedLinkError ignored) {
|
||||
}
|
||||
|
||||
// Try to find and open a stream to the packaged library resource.
|
||||
try {
|
||||
String library = System.mapLibraryName( name );
|
||||
int libraryDot = library.lastIndexOf( EXTENSION_SEPARATOR );
|
||||
String libraryName = (libraryDot > 0)? library.substring( 0, libraryDot ): library;
|
||||
String libraryExtension = (libraryDot > 0)? library.substring( libraryDot ): ".lib";
|
||||
String libraryResource = getLibraryResource( library );
|
||||
InputStream libraryStream = context.getResourceAsStream( libraryResource );
|
||||
if (libraryStream == null)
|
||||
throw new IllegalStateException(
|
||||
"Library: " + name + " (" + libraryResource + "), not found in class loader for: " + context );
|
||||
String library = System.mapLibraryName( name );
|
||||
int libraryDot = library.lastIndexOf( EXTENSION_SEPARATOR );
|
||||
String libraryName = (libraryDot > 0)? library.substring( 0, libraryDot ): library;
|
||||
String libraryExtension = (libraryDot > 0)? library.substring( libraryDot ): ".lib";
|
||||
|
||||
// Write the library resource to a temporary file.
|
||||
File libraryFile = File.createTempFile( libraryName, libraryExtension );
|
||||
FileOutputStream libraryFileStream = new FileOutputStream( libraryFile );
|
||||
@Nullable
|
||||
File libraryFile = null;
|
||||
Set<String> libraryResources = getLibraryResources( library );
|
||||
for (final String libraryResource : libraryResources) {
|
||||
try {
|
||||
libraryFile.deleteOnExit();
|
||||
ByteStreams.copy( libraryStream, libraryFileStream );
|
||||
}
|
||||
finally {
|
||||
libraryFileStream.close();
|
||||
libraryStream.close();
|
||||
}
|
||||
InputStream libraryStream = context.getResourceAsStream( libraryResource );
|
||||
if (libraryStream == null) {
|
||||
logger.dbg( "No resource for library: %s", libraryResource );
|
||||
continue;
|
||||
}
|
||||
|
||||
// Load the library from the temporary file.
|
||||
System.load( libraryFile.getAbsolutePath() );
|
||||
}
|
||||
catch (final IOException e) {
|
||||
throw new IllegalStateException( "Couldn't extract library: " + name, e );
|
||||
// Write the library resource to a temporary file.
|
||||
libraryFile = File.createTempFile( libraryName, libraryExtension );
|
||||
FileOutputStream libraryFileStream = new FileOutputStream( libraryFile );
|
||||
try {
|
||||
libraryFile.deleteOnExit();
|
||||
ByteStreams.copy( libraryStream, libraryFileStream );
|
||||
}
|
||||
finally {
|
||||
libraryFileStream.close();
|
||||
libraryStream.close();
|
||||
}
|
||||
|
||||
// Load the library from the temporary file.
|
||||
System.load( libraryFile.getAbsolutePath() );
|
||||
return true;
|
||||
}
|
||||
catch (@SuppressWarnings("ErrorNotRethrown") final IOException | UnsatisfiedLinkError e) {
|
||||
logger.dbg( e, "Couldn't load library: %s", libraryResource );
|
||||
|
||||
if (libraryFile != null)
|
||||
if (libraryFile.exists() && !libraryFile.delete())
|
||||
logger.wrn( "Couldn't clean up library file: %s", libraryFile );
|
||||
libraryFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static String getLibraryResource(final String library) {
|
||||
String system = System.getProperty( "os.name" ).toLowerCase( Locale.ROOT );
|
||||
String architecture = System.getProperty( "os.arch" ).toLowerCase( Locale.ROOT );
|
||||
if ("Mac OS X".equalsIgnoreCase( system ))
|
||||
system = "macos";
|
||||
private static Set<String> getLibraryResources(final String library) {
|
||||
// Standardize system naming in accordance with masterpassword-core.
|
||||
Sys system = Sys.findCurrent();
|
||||
|
||||
return Joiner.on( RESOURCE_SEPARATOR ).join( "", NATIVES_PATH, system, architecture, library );
|
||||
// Standardize architecture naming in accordance with masterpassword-core.
|
||||
Collection<Arch> architectures = new LinkedHashSet<>();
|
||||
architectures.add( Arch.findCurrent() );
|
||||
architectures.addAll( Arrays.asList( Arch.values() ) );
|
||||
|
||||
ImmutableSet.Builder<String> resources = ImmutableSet.builder();
|
||||
for (final Arch arch : architectures)
|
||||
resources.add( Joiner.on( RESOURCE_SEPARATOR ).join( "", NATIVES_PATH, system, arch, library ) );
|
||||
|
||||
return resources.build();
|
||||
}
|
||||
|
||||
private enum Sys implements Predicate<String> {
|
||||
windows {
|
||||
@Override
|
||||
public boolean test(final String system) {
|
||||
return system.contains( "windows" );
|
||||
}
|
||||
},
|
||||
macos {
|
||||
@Override
|
||||
public boolean test(final String system) {
|
||||
return system.contains( "mac os x" ) || system.contains( "darwin" ) || system.contains( "osx" );
|
||||
}
|
||||
},
|
||||
linux {
|
||||
@Override
|
||||
public boolean test(final String system) {
|
||||
return system.contains( "linux" );
|
||||
}
|
||||
};
|
||||
|
||||
@Nonnull
|
||||
public static Sys findCurrent() {
|
||||
return find( System.getProperty( "os.name" ) );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static Sys find(@Nullable String name) {
|
||||
if (name != null) {
|
||||
name = name.toLowerCase( Locale.ROOT );
|
||||
|
||||
for (final Sys sys : values())
|
||||
if (sys.test( name ))
|
||||
return sys;
|
||||
}
|
||||
|
||||
return linux;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private enum Arch implements Predicate<String> {
|
||||
arm {
|
||||
@Override
|
||||
public boolean test(final String architecture) {
|
||||
return ImmutableList.of( "arm", "arm-v7", "armv7", "arm32" ).contains( architecture );
|
||||
}
|
||||
},
|
||||
arm64 {
|
||||
@Override
|
||||
public boolean test(final String architecture) {
|
||||
return architecture.startsWith( "arm" ) && !arm.test( architecture );
|
||||
}
|
||||
},
|
||||
x86_64 {
|
||||
@Override
|
||||
public boolean test(final String architecture) {
|
||||
return ImmutableList.of( "x86_64", "amd64", "x64", "x86-64" ).contains( architecture );
|
||||
}
|
||||
},
|
||||
x86 {
|
||||
@Override
|
||||
public boolean test(final String architecture) {
|
||||
return ImmutableList.of( "x86", "i386", "i686" ).contains( architecture );
|
||||
}
|
||||
};
|
||||
|
||||
@Nonnull
|
||||
public static Arch findCurrent() {
|
||||
return find( System.getProperty( "os.arch" ) );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static Arch find(@Nullable String name) {
|
||||
if (name != null) {
|
||||
name = name.toLowerCase( Locale.ROOT );
|
||||
|
||||
for (final Arch arch : values())
|
||||
if (arch.test( name ))
|
||||
return arch;
|
||||
}
|
||||
|
||||
return x86;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.lyndir.masterpassword.util;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-25
|
||||
*/
|
||||
public final class Utilities {
|
||||
|
||||
@Nullable
|
||||
public static <T, R> R ifNotNull(@Nullable final T value, final Function<T, R> consumer) {
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
return consumer.apply( value );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static <T> T ifNotNullElse(@Nullable final T value, @Nonnull final T nullValue) {
|
||||
if (value == null)
|
||||
return nullValue;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static <T, R> R ifNotNullElse(@Nullable final T value, final Function<T, R> consumer, @Nonnull final R nullValue) {
|
||||
if (value == null)
|
||||
return nullValue;
|
||||
|
||||
return consumer.apply( value );
|
||||
}
|
||||
|
||||
public static <T> void ifNotNullDo(@Nullable final T value, final Consumer<T> consumer) {
|
||||
if (value != null)
|
||||
consumer.accept( value );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @author lhunath, 2018-07-25
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
package com.lyndir.masterpassword.util;
|
||||
|
||||
import javax.annotation.ParametersAreNonnullByDefault;
|
||||
@@ -5,32 +5,37 @@ plugins {
|
||||
}
|
||||
|
||||
description = 'Master Password GUI'
|
||||
mainClassName = 'com.lyndir.masterpassword.gui.GUI'
|
||||
mainClassName = 'com.lyndir.masterpassword.gui.MasterPassword'
|
||||
|
||||
dependencies {
|
||||
implementation group: 'com.lyndir.lhunath.opal', name: 'opal-system', version: '1.7-p2'
|
||||
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.1.2'
|
||||
implementation group: 'com.yuvimasory', name: 'orange-extensions', version: '1.3.0'
|
||||
implementation group: 'com.github.tulskiy', name: 'jkeymaster', version: '1.2'
|
||||
|
||||
compile project( ':masterpassword-model' )
|
||||
}
|
||||
|
||||
|
||||
// release with: STORE_PW=$(mpw masterpassword.keystore) KEY_PW_ANDROID=$(mpw masterpassword-android) gradle masterpassword-gui:shadowJar
|
||||
shadowJar.doLast {
|
||||
if (System.getenv( 'KEY_PW_DESKTOP' ) != null)
|
||||
ant.signjar(
|
||||
jar: archivePath,
|
||||
alias: 'masterpassword-desktop',
|
||||
keystore: 'masterpassword.keystore',
|
||||
storepass: System.getenv( 'STORE_PW' ),
|
||||
keypass: System.getenv( 'KEY_PW_DESKTOP' ),
|
||||
preservelastmodified: 'true',
|
||||
destdir: '.'
|
||||
)
|
||||
// release with: STORE_PW=$(mpw masterpassword.keystore) KEY_PW_DESKTOP=$(mpw masterpassword-desktop) gradle clean masterpassword-gui:shadowJar
|
||||
shadowJar {
|
||||
manifest {
|
||||
attributes 'Implementation-Title': description
|
||||
attributes 'Implementation-Version': version
|
||||
}
|
||||
doLast {
|
||||
if (System.getenv( 'KEY_PW_DESKTOP' ) != null)
|
||||
ant.signjar( jar: archivePath,
|
||||
alias: 'masterpassword-desktop',
|
||||
keystore: 'masterpassword.keystore',
|
||||
storepass: System.getenv( 'STORE_PW' ),
|
||||
keypass: System.getenv( 'KEY_PW_DESKTOP' ),
|
||||
preservelastmodified: 'true',
|
||||
destdir: '.' )
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
// I don't fully understand why this is necessary, but without it -Dmp.log.level is lost.
|
||||
systemProperties = System.properties
|
||||
//systemProperties = System.properties
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
//==============================================================================
|
||||
// This file is part of Master Password.
|
||||
// Copyright (c) 2011-2017, Maarten Billemont.
|
||||
//
|
||||
// Master Password is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Master Password is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You can find a copy of the GNU General Public License in the
|
||||
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
|
||||
//==============================================================================
|
||||
|
||||
package com.lyndir.masterpassword.gui;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.io.CharSource;
|
||||
import com.google.common.io.Resources;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import com.lyndir.lhunath.opal.system.util.TypeUtils;
|
||||
import com.lyndir.masterpassword.gui.view.PasswordFrame;
|
||||
import com.lyndir.masterpassword.gui.view.UnlockFrame;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Optional;
|
||||
import java.util.jar.*;
|
||||
import javax.swing.*;
|
||||
|
||||
|
||||
/**
|
||||
* <p> <i>Jun 10, 2008</i> </p>
|
||||
*
|
||||
* @author mbillemo
|
||||
*/
|
||||
public class GUI implements UnlockFrame.SignInCallback {
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
private static final Logger logger = Logger.get( GUI.class );
|
||||
|
||||
private final UnlockFrame unlockFrame = new UnlockFrame( this );
|
||||
private PasswordFrame<?, ?> passwordFrame;
|
||||
|
||||
public static void main(final String... args) {
|
||||
if (Config.get().checkForUpdates())
|
||||
checkUpdate();
|
||||
|
||||
// Try and set the system look & feel, if available.
|
||||
try {
|
||||
UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
|
||||
}
|
||||
catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
// AppleGUI adds support for macOS features.
|
||||
Optional<Class<GUI>> appleGUI = TypeUtils.loadClass( "com.lyndir.masterpassword.gui.platform.mac.AppleGUI" );
|
||||
if (appleGUI.isPresent())
|
||||
appleGUI.get().getConstructor().newInstance().open();
|
||||
|
||||
else // No special platform handling.
|
||||
new GUI().open();
|
||||
}
|
||||
catch (final IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
|
||||
throw logger.bug( e );
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkUpdate() {
|
||||
try {
|
||||
Enumeration<URL> manifestURLs = Thread.currentThread().getContextClassLoader().getResources( JarFile.MANIFEST_NAME );
|
||||
while (manifestURLs.hasMoreElements())
|
||||
try (InputStream manifestStream = manifestURLs.nextElement().openStream()) {
|
||||
Attributes attributes = new Manifest( manifestStream ).getMainAttributes();
|
||||
if (!GUI.class.getCanonicalName().equals( attributes.getValue( Attributes.Name.MAIN_CLASS ) ))
|
||||
continue;
|
||||
|
||||
String manifestRevision = attributes.getValue( Attributes.Name.IMPLEMENTATION_VERSION );
|
||||
String upstreamRevisionURL = "https://masterpassword.app/masterpassword-gui.jar.rev";
|
||||
CharSource upstream = Resources.asCharSource( URI.create( upstreamRevisionURL ).toURL(), Charsets.UTF_8 );
|
||||
String upstreamRevision = upstream.readFirstLine();
|
||||
if ((manifestRevision != null) && (upstreamRevision != null) && !manifestRevision.equalsIgnoreCase(
|
||||
upstreamRevision )) {
|
||||
logger.inf( "Local Revision: <%s>", manifestRevision );
|
||||
logger.inf( "Upstream Revision: <%s>", upstreamRevision );
|
||||
logger.wrn( "You are not running the current official version. Please update from:%n%s",
|
||||
"https://masterpassword.app/masterpassword-gui.jar" );
|
||||
JOptionPane.showMessageDialog( null,
|
||||
strf( "A new version of Master Password is available.%n "
|
||||
+ "Please download the latest version from %s",
|
||||
"https://masterpassword.app" ),
|
||||
"Update Available", JOptionPane.WARNING_MESSAGE );
|
||||
}
|
||||
}
|
||||
catch (final IOException e) {
|
||||
logger.wrn( e, "Couldn't check for version update." );
|
||||
}
|
||||
}
|
||||
catch (final IOException e) {
|
||||
logger.wrn( e, "Couldn't inspect JAR." );
|
||||
}
|
||||
}
|
||||
|
||||
protected void open() {
|
||||
SwingUtilities.invokeLater( () -> {
|
||||
if (passwordFrame == null)
|
||||
unlockFrame.setVisible( true );
|
||||
else
|
||||
passwordFrame.setVisible( true );
|
||||
} );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void signedIn(final PasswordFrame<?, ?> passwordFrame) {
|
||||
this.passwordFrame = passwordFrame;
|
||||
open();
|
||||
}
|
||||
}
|
||||
@@ -19,22 +19,22 @@
|
||||
package com.lyndir.masterpassword.gui;
|
||||
|
||||
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
|
||||
import com.lyndir.masterpassword.model.MPConstants;
|
||||
import com.lyndir.masterpassword.model.MPModelConstants;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2014-08-31
|
||||
*/
|
||||
@SuppressWarnings("CallToSystemGetenv")
|
||||
public class Config {
|
||||
public class MPConfig {
|
||||
|
||||
private static final Config instance = new Config();
|
||||
private static final MPConfig instance = new MPConfig();
|
||||
|
||||
public static Config get() {
|
||||
public static MPConfig get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public boolean checkForUpdates() {
|
||||
return ConversionUtils.toBoolean( System.getenv( MPConstants.env_checkUpdates ) ).orElse( true );
|
||||
return ConversionUtils.toBoolean( System.getenv( MPModelConstants.env_checkUpdates ) ).orElse( true );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.lyndir.masterpassword.gui;
|
||||
|
||||
import java.awt.event.InputEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import javax.swing.*;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-31
|
||||
*/
|
||||
public final class MPGuiConstants {
|
||||
|
||||
public static final KeyStroke ui_hotkey = KeyStroke.getKeyStroke( KeyEvent.VK_P, InputEvent.CTRL_DOWN_MASK | InputEvent.META_DOWN_MASK );
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
//==============================================================================
|
||||
// This file is part of Master Password.
|
||||
// Copyright (c) 2011-2017, Maarten Billemont.
|
||||
//
|
||||
// Master Password is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Master Password is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You can find a copy of the GNU General Public License in the
|
||||
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
|
||||
//==============================================================================
|
||||
|
||||
package com.lyndir.masterpassword.gui;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.io.ByteSource;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import com.lyndir.lhunath.opal.system.util.ObjectUtils;
|
||||
import com.lyndir.masterpassword.gui.util.*;
|
||||
import com.lyndir.masterpassword.gui.view.MasterPasswordFrame;
|
||||
import com.lyndir.masterpassword.model.MPUser;
|
||||
import com.tulskiy.keymaster.common.Provider;
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.*;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.*;
|
||||
|
||||
|
||||
/**
|
||||
* <p> <i>Jun 10, 2008</i> </p>
|
||||
*
|
||||
* @author mbillemo
|
||||
*/
|
||||
public final class MasterPassword {
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
private static final Logger logger = Logger.get( MasterPassword.class );
|
||||
|
||||
private static final MasterPassword instance = new MasterPassword();
|
||||
|
||||
private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
@Nullable
|
||||
private MasterPasswordFrame frame;
|
||||
@Nullable
|
||||
private MPUser<?> activeUser;
|
||||
|
||||
public static MasterPassword get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void addListener(final Listener listener) {
|
||||
if (listeners.add( listener ))
|
||||
listener.onUserSelected( activeUser );
|
||||
}
|
||||
|
||||
public void removeListener(final Listener listener) {
|
||||
listeners.remove( listener );
|
||||
}
|
||||
|
||||
public void activateUser(final MPUser<?> user) {
|
||||
if (ObjectUtils.equals( activeUser, user ))
|
||||
return;
|
||||
|
||||
activeUser = user;
|
||||
for (final Listener listener : listeners)
|
||||
listener.onUserSelected( activeUser );
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String version() {
|
||||
return MasterPassword.class.getPackage().getImplementationVersion();
|
||||
}
|
||||
|
||||
public void open() {
|
||||
Res.ui( () -> {
|
||||
if (frame == null)
|
||||
frame = new MasterPasswordFrame();
|
||||
|
||||
frame.setAlwaysOnTop( true );
|
||||
frame.setVisible( true );
|
||||
frame.setExtendedState( Frame.NORMAL );
|
||||
Platform.get().requestForeground();
|
||||
frame.setAlwaysOnTop( false );
|
||||
} );
|
||||
}
|
||||
|
||||
public void checkUpdate() {
|
||||
try {
|
||||
String implementationVersion = version();
|
||||
String latestVersion = new ByteSource() {
|
||||
@Override
|
||||
public InputStream openStream()
|
||||
throws IOException {
|
||||
URL url = URI.create( "https://masterpassword.app/masterpassword-gui.jar.rev" ).toURL();
|
||||
URLConnection conn = url.openConnection();
|
||||
conn.addRequestProperty( "User-Agent", "masterpassword-gui" );
|
||||
return conn.getInputStream();
|
||||
}
|
||||
}.asCharSource( Charsets.UTF_8 ).readFirstLine();
|
||||
|
||||
if ((implementationVersion != null) && !implementationVersion.equalsIgnoreCase( latestVersion )) {
|
||||
logger.inf( "Implementation: <%s>", implementationVersion );
|
||||
logger.inf( "Latest : <%s>", latestVersion );
|
||||
logger.wrn( "You are not running the current official version. Please update from:%n%s",
|
||||
"https://masterpassword.app/masterpassword-gui.jar" );
|
||||
JOptionPane.showMessageDialog( null, Components.linkLabel( strf(
|
||||
"A new version of Master Password is available."
|
||||
+ "<p>Please download the latest version from <a href='https://masterpassword.app'>https://masterpassword.app</a>." ) ),
|
||||
"Update Available", JOptionPane.INFORMATION_MESSAGE );
|
||||
}
|
||||
}
|
||||
catch (final IOException e) {
|
||||
logger.wrn( e, "Couldn't check for version update." );
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(final String... args) {
|
||||
//Thread.setDefaultUncaughtExceptionHandler(
|
||||
// (t, e) -> logger.bug( e, "Uncaught: %s", e.getLocalizedMessage() ) );
|
||||
|
||||
// Try and set the system look & feel, if available.
|
||||
try {
|
||||
UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
|
||||
Platform.get().installAppForegroundHandler( get()::open );
|
||||
Platform.get().installAppReopenHandler( get()::open );
|
||||
Provider.getCurrentProvider( true ).register( MPGuiConstants.ui_hotkey, hotKey -> get().open() );
|
||||
}
|
||||
catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
|
||||
}
|
||||
|
||||
// Create a platform-specific GUI and open it.
|
||||
get().open();
|
||||
|
||||
// Check online to see if this version has been superseded.
|
||||
if (MPConfig.get().checkForUpdates())
|
||||
get().checkUpdate();
|
||||
}
|
||||
|
||||
@SuppressWarnings("InterfaceMayBeAnnotatedFunctional")
|
||||
public interface Listener {
|
||||
|
||||
void onUserSelected(@Nullable MPUser<?> user);
|
||||
}
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
//==============================================================================
|
||||
// This file is part of Master Password.
|
||||
// Copyright (c) 2011-2017, Maarten Billemont.
|
||||
//
|
||||
// Master Password is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Master Password is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You can find a copy of the GNU General Public License in the
|
||||
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
|
||||
//==============================================================================
|
||||
|
||||
package com.lyndir.masterpassword.gui;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.common.util.concurrent.JdkFutureAdapters;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import com.lyndir.masterpassword.MPIdenticon;
|
||||
import java.awt.*;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.image.ImageObserver;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.net.URL;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.swing.*;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2014-06-11
|
||||
*/
|
||||
@SuppressWarnings({ "HardcodedFileSeparator", "MethodReturnAlwaysConstant", "SpellCheckingInspection" })
|
||||
public abstract class Res {
|
||||
|
||||
private static final int AVATAR_COUNT = 19;
|
||||
private static final Map<Window, ScheduledExecutorService> executorByWindow = new WeakHashMap<>();
|
||||
private static final Logger logger = Logger.get( Res.class );
|
||||
private static final Colors colors = new Colors();
|
||||
|
||||
public static Future<?> execute(final Window host, final Runnable job) {
|
||||
return schedule( host, job, 0, TimeUnit.MILLISECONDS );
|
||||
}
|
||||
|
||||
public static Future<?> schedule(final Window host, final Runnable job, final long delay, final TimeUnit timeUnit) {
|
||||
return getExecutor( host ).schedule( () -> {
|
||||
try {
|
||||
job.run();
|
||||
}
|
||||
catch (final Throwable t) {
|
||||
logger.err( t, "Unexpected: %s", t.getLocalizedMessage() );
|
||||
}
|
||||
}, delay, timeUnit );
|
||||
}
|
||||
|
||||
public static <V> ListenableFuture<V> execute(final Window host, final Callable<V> job) {
|
||||
return schedule( host, job, 0, TimeUnit.MILLISECONDS );
|
||||
}
|
||||
|
||||
public static <V> ListenableFuture<V> schedule(final Window host, final Callable<V> job, final long delay, final TimeUnit timeUnit) {
|
||||
ScheduledExecutorService executor = getExecutor( host );
|
||||
return JdkFutureAdapters.listenInPoolThread( executor.schedule( () -> {
|
||||
try {
|
||||
return job.call();
|
||||
}
|
||||
catch (final Throwable t) {
|
||||
logger.err( t, "Unexpected: %s", t.getLocalizedMessage() );
|
||||
throw Throwables.propagate( t );
|
||||
}
|
||||
}, delay, timeUnit ), executor );
|
||||
}
|
||||
|
||||
private static ScheduledExecutorService getExecutor(final Window host) {
|
||||
ScheduledExecutorService executor = executorByWindow.get( host );
|
||||
|
||||
if (executor == null) {
|
||||
executorByWindow.put( host, executor = Executors.newSingleThreadScheduledExecutor() );
|
||||
|
||||
host.addWindowListener( new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(final WindowEvent e) {
|
||||
ExecutorService executor = executorByWindow.remove( host );
|
||||
if (executor != null)
|
||||
executor.shutdownNow();
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
return executor;
|
||||
}
|
||||
|
||||
public static Icon iconAdd() {
|
||||
return new RetinaIcon( Resources.getResource( "media/icon_add@2x.png" ) );
|
||||
}
|
||||
|
||||
public static Icon iconDelete() {
|
||||
return new RetinaIcon( Resources.getResource( "media/icon_delete@2x.png" ) );
|
||||
}
|
||||
|
||||
public static Icon iconQuestion() {
|
||||
return new RetinaIcon( Resources.getResource( "media/icon_question@2x.png" ) );
|
||||
}
|
||||
|
||||
public static Icon avatar(final int index) {
|
||||
return new RetinaIcon( Resources.getResource( strf( "media/avatar-%d@2x.png", index % avatars() ) ) );
|
||||
}
|
||||
|
||||
public static int avatars() {
|
||||
return AVATAR_COUNT;
|
||||
}
|
||||
|
||||
public static Font emoticonsFont() {
|
||||
return emoticonsRegular();
|
||||
}
|
||||
|
||||
public static Font controlFont() {
|
||||
return arimoRegular();
|
||||
}
|
||||
|
||||
public static Font valueFont() {
|
||||
return sourceSansProRegular();
|
||||
}
|
||||
|
||||
public static Font bigValueFont() {
|
||||
return sourceSansProBlack();
|
||||
}
|
||||
|
||||
public static Font emoticonsRegular() {
|
||||
return font( "fonts/Emoticons-Regular.otf" );
|
||||
}
|
||||
|
||||
public static Font sourceCodeProRegular() {
|
||||
return font( "fonts/SourceCodePro-Regular.otf" );
|
||||
}
|
||||
|
||||
public static Font sourceCodeProBlack() {
|
||||
return font( "fonts/SourceCodePro-Bold.otf" );
|
||||
}
|
||||
|
||||
public static Font sourceSansProRegular() {
|
||||
return font( "fonts/SourceSansPro-Regular.otf" );
|
||||
}
|
||||
|
||||
public static Font sourceSansProBlack() {
|
||||
return font( "fonts/SourceSansPro-Bold.otf" );
|
||||
}
|
||||
|
||||
public static Font exoBold() {
|
||||
return font( "fonts/Exo2.0-Bold.otf" );
|
||||
}
|
||||
|
||||
public static Font exoExtraBold() {
|
||||
return font( "fonts/Exo2.0-ExtraBold.otf" );
|
||||
}
|
||||
|
||||
public static Font exoRegular() {
|
||||
return font( "fonts/Exo2.0-Regular.otf" );
|
||||
}
|
||||
|
||||
public static Font exoThin() {
|
||||
return font( "fonts/Exo2.0-Thin.otf" );
|
||||
}
|
||||
|
||||
public static Font arimoBold() {
|
||||
return font( "fonts/Arimo-Bold.ttf" );
|
||||
}
|
||||
|
||||
public static Font arimoBoldItalic() {
|
||||
return font( "fonts/Arimo-BoldItalic.ttf" );
|
||||
}
|
||||
|
||||
public static Font arimoItalic() {
|
||||
return font( "fonts/Arimo-Italic.ttf" );
|
||||
}
|
||||
|
||||
public static Font arimoRegular() {
|
||||
return font( "fonts/Arimo-Regular.ttf" );
|
||||
}
|
||||
|
||||
private static Font font(@NonNls final String fontResourceName) {
|
||||
Map<String, SoftReference<Font>> fontsByResourceName = Maps.newHashMap();
|
||||
SoftReference<Font> fontRef = fontsByResourceName.get( fontResourceName );
|
||||
Font font = (fontRef == null)? null: fontRef.get();
|
||||
if (font == null)
|
||||
try {
|
||||
fontsByResourceName.put( fontResourceName, new SoftReference<>(
|
||||
font = Font.createFont( Font.TRUETYPE_FONT, Resources.getResource( fontResourceName ).openStream() ) ) );
|
||||
}
|
||||
catch (final FontFormatException | IOException e) {
|
||||
throw Throwables.propagate( e );
|
||||
}
|
||||
|
||||
return font;
|
||||
}
|
||||
|
||||
public static Colors colors() {
|
||||
return colors;
|
||||
}
|
||||
|
||||
private static final class RetinaIcon extends ImageIcon {
|
||||
|
||||
private static final Pattern scalePattern = Pattern.compile( ".*@(\\d+)x.[^.]+$" );
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final float scale;
|
||||
|
||||
private RetinaIcon(final URL url) {
|
||||
super( url );
|
||||
|
||||
Matcher scaleMatcher = scalePattern.matcher( url.getPath() );
|
||||
scale = scaleMatcher.matches()? Float.parseFloat( scaleMatcher.group( 1 ) ): 1;
|
||||
}
|
||||
|
||||
//private static URL retinaURL(final URL url) {
|
||||
// try {
|
||||
// final boolean[] isRetina = new boolean[1];
|
||||
// new apple.awt.CImage.HiDPIScaledImage(1,1, BufferedImage.TYPE_INT_ARGB) {
|
||||
// @Override
|
||||
// public void drawIntoImage(BufferedImage image, float v) {
|
||||
// isRetina[0] = v > 1;
|
||||
// }
|
||||
// };
|
||||
// return isRetina[0];
|
||||
// } catch (Throwable e) {
|
||||
// e.printStackTrace();
|
||||
// return url;
|
||||
// }
|
||||
//}
|
||||
|
||||
@Override
|
||||
public int getIconWidth() {
|
||||
return (int) (super.getIconWidth() / scale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconHeight() {
|
||||
return (int) (super.getIconHeight() / scale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void paintIcon(final Component c, final Graphics g, final int x, final int y) {
|
||||
ImageObserver observer = ifNotNullElse( getImageObserver(), c );
|
||||
|
||||
Image image = getImage();
|
||||
int width = image.getWidth( observer );
|
||||
int height = image.getHeight( observer );
|
||||
Graphics2D g2d = (Graphics2D) g.create( x, y, width, height );
|
||||
|
||||
g2d.scale( 1 / scale, 1 / scale );
|
||||
g2d.drawImage( image, 0, 0, observer );
|
||||
g2d.scale( 1, 1 );
|
||||
g2d.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class Colors {
|
||||
|
||||
private final Color frameBg = Color.decode( "#5A5D6B" );
|
||||
private final Color controlBg = Color.decode( "#ECECEC" );
|
||||
private final Color controlBorder = Color.decode( "#BFBFBF" );
|
||||
|
||||
public Color frameBg() {
|
||||
return frameBg;
|
||||
}
|
||||
|
||||
public Color controlBg() {
|
||||
return controlBg;
|
||||
}
|
||||
|
||||
public Color controlBorder() {
|
||||
return controlBorder;
|
||||
}
|
||||
|
||||
public Color fromIdenticonColor(final MPIdenticon.Color identiconColor, final BackgroundMode backgroundMode) {
|
||||
switch (identiconColor) {
|
||||
case RED:
|
||||
switch (backgroundMode) {
|
||||
case DARK:
|
||||
return Color.decode( "#dc322f" );
|
||||
case LIGHT:
|
||||
return Color.decode( "#dc322f" );
|
||||
}
|
||||
break;
|
||||
case GREEN:
|
||||
switch (backgroundMode) {
|
||||
case DARK:
|
||||
return Color.decode( "#859900" );
|
||||
case LIGHT:
|
||||
return Color.decode( "#859900" );
|
||||
}
|
||||
break;
|
||||
case YELLOW:
|
||||
switch (backgroundMode) {
|
||||
case DARK:
|
||||
return Color.decode( "#b58900" );
|
||||
case LIGHT:
|
||||
return Color.decode( "#b58900" );
|
||||
}
|
||||
break;
|
||||
case BLUE:
|
||||
switch (backgroundMode) {
|
||||
case DARK:
|
||||
return Color.decode( "#268bd2" );
|
||||
case LIGHT:
|
||||
return Color.decode( "#268bd2" );
|
||||
}
|
||||
break;
|
||||
case MAGENTA:
|
||||
switch (backgroundMode) {
|
||||
case DARK:
|
||||
return Color.decode( "#d33682" );
|
||||
case LIGHT:
|
||||
return Color.decode( "#d33682" );
|
||||
}
|
||||
break;
|
||||
case CYAN:
|
||||
switch (backgroundMode) {
|
||||
case DARK:
|
||||
return Color.decode( "#2aa198" );
|
||||
case LIGHT:
|
||||
return Color.decode( "#2aa198" );
|
||||
}
|
||||
break;
|
||||
case MONO:
|
||||
switch (backgroundMode) {
|
||||
case DARK:
|
||||
return Color.decode( "#93a1a1" );
|
||||
case LIGHT:
|
||||
return Color.decode( "#586e75" );
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException( strf( "Color: %s or mode: %s not supported: ", identiconColor, backgroundMode ) );
|
||||
}
|
||||
|
||||
public enum BackgroundMode {
|
||||
DARK, LIGHT
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,17 +31,7 @@ import javax.annotation.Nullable;
|
||||
*/
|
||||
public class MPIncognitoQuestion extends MPBasicQuestion {
|
||||
|
||||
private final MPIncognitoSite site;
|
||||
|
||||
public MPIncognitoQuestion(final MPIncognitoSite site, final String keyword, @Nullable final MPResultType type) {
|
||||
super( keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) );
|
||||
|
||||
this.site = site;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public MPIncognitoSite getSite() {
|
||||
return site;
|
||||
super( site, keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,30 +22,28 @@ import com.google.common.primitives.UnsignedInteger;
|
||||
import com.lyndir.masterpassword.MPAlgorithm;
|
||||
import com.lyndir.masterpassword.MPResultType;
|
||||
import com.lyndir.masterpassword.model.impl.MPBasicSite;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 14-12-16
|
||||
*/
|
||||
public class MPIncognitoSite extends MPBasicSite<MPIncognitoQuestion> {
|
||||
public class MPIncognitoSite extends MPBasicSite<MPIncognitoUser, MPIncognitoQuestion> {
|
||||
|
||||
private final MPIncognitoUser user;
|
||||
|
||||
public MPIncognitoSite(final MPIncognitoUser user, final String name) {
|
||||
this( user, name, null, null, null, null );
|
||||
public MPIncognitoSite(final MPIncognitoUser user, final String siteName) {
|
||||
this( user, siteName, null, null, null, null );
|
||||
}
|
||||
|
||||
public MPIncognitoSite(final MPIncognitoUser user, final String name,
|
||||
public MPIncognitoSite(final MPIncognitoUser user, final String siteName,
|
||||
@Nullable final MPAlgorithm algorithm, @Nullable final UnsignedInteger counter,
|
||||
@Nullable final MPResultType resultType, @Nullable final MPResultType loginType) {
|
||||
super( name, (algorithm == null)? user.getAlgorithm(): algorithm, counter, resultType, loginType );
|
||||
|
||||
this.user = user;
|
||||
super( user, siteName, (algorithm == null)? user.getAlgorithm(): algorithm, counter, resultType, loginType );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public MPIncognitoUser getUser() {
|
||||
return user;
|
||||
public MPIncognitoQuestion addQuestion(final String keyword) {
|
||||
return addQuestion( new MPIncognitoQuestion( this, keyword, null ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package com.lyndir.masterpassword.gui.model;
|
||||
|
||||
import com.lyndir.masterpassword.MPAlgorithm;
|
||||
import com.lyndir.masterpassword.model.impl.MPBasicUser;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
@@ -37,4 +38,10 @@ public class MPIncognitoUser extends MPBasicUser<MPIncognitoSite> {
|
||||
public byte[] getKeyID() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public MPIncognitoSite addSite(final String siteName) {
|
||||
return addSite( new MPIncognitoSite( this, siteName ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.lyndir.masterpassword.gui.model;
|
||||
|
||||
import com.lyndir.masterpassword.model.MPSite;
|
||||
import com.lyndir.masterpassword.model.impl.MPBasicQuestion;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-27
|
||||
*/
|
||||
public class MPNewQuestion extends MPBasicQuestion {
|
||||
|
||||
public MPNewQuestion(final MPSite<?> site, final String keyword) {
|
||||
super( site, keyword, site.getAlgorithm().mpw_default_answer_type() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.lyndir.masterpassword.gui.model;
|
||||
|
||||
import com.lyndir.masterpassword.model.*;
|
||||
import com.lyndir.masterpassword.model.impl.*;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-27
|
||||
*/
|
||||
public class MPNewSite extends MPBasicSite<MPUser<?>, MPQuestion> {
|
||||
|
||||
public MPNewSite(final MPUser<?> user, final String siteName) {
|
||||
super( user, siteName );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public MPQuestion addQuestion(final String keyword) {
|
||||
throw new UnsupportedOperationException( "Cannot add a question to a site that hasn't been created yet." );
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
//==============================================================================
|
||||
// This file is part of Master Password.
|
||||
// Copyright (c) 2011-2017, Maarten Billemont.
|
||||
//
|
||||
// Master Password is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Master Password is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You can find a copy of the GNU General Public License in the
|
||||
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
|
||||
//==============================================================================
|
||||
|
||||
package com.lyndir.masterpassword.gui.platform.mac;
|
||||
|
||||
import com.apple.eawt.*;
|
||||
import com.lyndir.masterpassword.gui.GUI;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2014-06-10
|
||||
*/
|
||||
public class AppleGUI extends GUI {
|
||||
|
||||
public AppleGUI() {
|
||||
|
||||
Application application = Application.getApplication();
|
||||
application.addAppEventListener( new AppForegroundListener() {
|
||||
|
||||
@Override
|
||||
public void appMovedToBackground(final AppEvent.AppForegroundEvent arg0) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appRaisedToForeground(final AppEvent.AppForegroundEvent arg0) {
|
||||
open();
|
||||
}
|
||||
} );
|
||||
application.addAppEventListener( new AppReOpenedListener() {
|
||||
@Override
|
||||
public void appReOpened(final AppEvent.AppReOpenedEvent arg0) {
|
||||
open();
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.lyndir.masterpassword.gui.util;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import com.lyndir.lhunath.opal.system.util.ObjectUtils;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
import javax.swing.event.ListSelectionListener;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-19
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class CollectionListModel<E> extends AbstractListModel<E>
|
||||
implements ComboBoxModel<E>, ListSelectionListener, Selectable<E, CollectionListModel<E>> {
|
||||
|
||||
private static final Logger logger = Logger.get( CollectionListModel.class );
|
||||
|
||||
private final List<E> model = new LinkedList<>();
|
||||
@Nullable
|
||||
private JList<E> list;
|
||||
@Nullable
|
||||
private E selectedItem;
|
||||
@Nullable
|
||||
private Consumer<E> selectionConsumer;
|
||||
|
||||
@SafeVarargs
|
||||
public CollectionListModel(final E... elements) {
|
||||
this( Arrays.asList( elements ) );
|
||||
}
|
||||
|
||||
public CollectionListModel(final Collection<? extends E> elements) {
|
||||
model.addAll( elements );
|
||||
selectedItem = getElementAt( 0 );
|
||||
fireIntervalAdded( this, 0, model.size() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int getSize() {
|
||||
return model.size();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public synchronized E getElementAt(final int index) {
|
||||
return (index < model.size())? model.get( index ): null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace this model's contents with the objects from the new model collection.
|
||||
*
|
||||
* This operation will mutate the internal model to reflect the given model.
|
||||
* The given model will remain untouched and independent from this object.
|
||||
*/
|
||||
@SuppressWarnings({ "Guava", "AssignmentToForLoopParameter" })
|
||||
public synchronized void set(final Iterable<? extends E> elements) {
|
||||
ListIterator<E> oldIt = model.listIterator();
|
||||
for (int from = 0; oldIt.hasNext(); ++from) {
|
||||
int to = Iterables.indexOf( elements, Predicates.equalTo( oldIt.next() ) );
|
||||
|
||||
if (to != from) {
|
||||
oldIt.remove();
|
||||
fireIntervalRemoved( this, from, from );
|
||||
--from;
|
||||
}
|
||||
}
|
||||
|
||||
int to = 0;
|
||||
for (final E newSite : elements) {
|
||||
if ((to >= model.size()) || !Objects.equals( model.get( to ), newSite )) {
|
||||
model.add( to, newSite );
|
||||
fireIntervalAdded( this, to, to );
|
||||
}
|
||||
|
||||
++to;
|
||||
}
|
||||
|
||||
if ((selectedItem == null) || !model.contains( selectedItem ))
|
||||
setSelectedItem( getElementAt( 0 ) );
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public final synchronized void set(final E... elements) {
|
||||
set( ImmutableList.copyOf( elements ) );
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "unchecked", "SuspiciousMethodCalls" })
|
||||
public synchronized void setSelectedItem(@Nullable final Object newSelectedItem) {
|
||||
if (!Objects.equals( selectedItem, newSelectedItem )) {
|
||||
selectedItem = (E) newSelectedItem;
|
||||
|
||||
fireContentsChanged( this, -1, -1 );
|
||||
//noinspection ObjectEquality
|
||||
if ((list != null) && (list.getModel() == this))
|
||||
list.setSelectedValue( selectedItem, true );
|
||||
|
||||
if (selectionConsumer != null)
|
||||
selectionConsumer.accept( selectedItem );
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public synchronized E getSelectedItem() {
|
||||
return selectedItem;
|
||||
}
|
||||
|
||||
public CollectionListModel<E> select(final E selectedItem) {
|
||||
setSelectedItem( selectedItem );
|
||||
return this;
|
||||
}
|
||||
|
||||
public synchronized void registerList(final JList<E> list) {
|
||||
// TODO: This class should probably implement ListSelectionModel instead.
|
||||
if (this.list != null)
|
||||
this.list.removeListSelectionListener( this );
|
||||
|
||||
this.list = list;
|
||||
this.list.addListSelectionListener( this );
|
||||
this.list.setModel( this );
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized CollectionListModel<E> selection(@Nullable final Consumer<E> selectionConsumer) {
|
||||
this.selectionConsumer = selectionConsumer;
|
||||
if (selectionConsumer != null)
|
||||
selectionConsumer.accept( selectedItem );
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized CollectionListModel<E> selection(@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
|
||||
this.selectionConsumer = null;
|
||||
setSelectedItem( selectedItem );
|
||||
|
||||
return selection( selectionConsumer );
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void valueChanged(final ListSelectionEvent event) {
|
||||
//noinspection ObjectEquality
|
||||
if (!event.getValueIsAdjusting() && (event.getSource() == list) && (list.getModel() == this)) {
|
||||
selectedItem = list.getSelectedValue();
|
||||
|
||||
if (selectionConsumer != null)
|
||||
selectionConsumer.accept( selectedItem );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,24 +18,51 @@
|
||||
|
||||
package com.lyndir.masterpassword.gui.util;
|
||||
|
||||
import com.lyndir.masterpassword.gui.Res;
|
||||
import com.google.common.base.Strings;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.io.File;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.Border;
|
||||
import javax.swing.border.CompoundBorder;
|
||||
import javax.swing.event.HyperlinkEvent;
|
||||
import javax.swing.text.*;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2014-06-08
|
||||
*/
|
||||
@SuppressWarnings({ "SerializableStoresNonSerializable", "serial" })
|
||||
public abstract class Components {
|
||||
|
||||
private static final float CONTROL_TEXT_SIZE = 12f;
|
||||
private static final Logger logger = Logger.get( Components.class );
|
||||
|
||||
public static GradientPanel boxLayout(final int axis, final Component... components) {
|
||||
GradientPanel container = gradientPanel( null, null );
|
||||
// container.setBackground( Color.red );
|
||||
public static final float TEXT_SIZE_HEADING = 19f;
|
||||
public static final float TEXT_SIZE_CONTROL = 13f;
|
||||
public static final int SIZE_MARGIN = 12;
|
||||
public static final int SIZE_PADDING = 8;
|
||||
|
||||
public static GradientPanel panel(final Component... components) {
|
||||
GradientPanel panel = panel( BoxLayout.LINE_AXIS, null, components );
|
||||
panel.setLayout( new OverlayLayout( panel ) );
|
||||
return panel;
|
||||
}
|
||||
|
||||
public static GradientPanel panel(final int axis, final Component... components) {
|
||||
return panel( axis, null, components );
|
||||
}
|
||||
|
||||
public static GradientPanel panel(final int axis, @Nullable final Color background, final Component... components) {
|
||||
GradientPanel container = panel( null, background );
|
||||
container.setLayout( new BoxLayout( container, axis ) );
|
||||
for (final Component component : components)
|
||||
container.add( component );
|
||||
@@ -43,46 +70,142 @@ public abstract class Components {
|
||||
return container;
|
||||
}
|
||||
|
||||
public static GradientPanel borderPanel(final JComponent component, @Nullable final Border border) {
|
||||
return borderPanel( component, border, null );
|
||||
public static GradientPanel borderPanel(final int axis, final Component... components) {
|
||||
return borderPanel( marginBorder(), null, axis, components );
|
||||
}
|
||||
|
||||
public static GradientPanel borderPanel(final JComponent component, @Nullable final Border border, @Nullable final Color background) {
|
||||
GradientPanel box = boxLayout( BoxLayout.LINE_AXIS, component );
|
||||
public static GradientPanel borderPanel(@Nullable final Border border, final int axis, final Component... components) {
|
||||
return borderPanel( border, null, axis, components );
|
||||
}
|
||||
|
||||
public static GradientPanel borderPanel(@Nullable final Color background, final int axis, final Component... components) {
|
||||
return borderPanel( marginBorder(), background, axis, components );
|
||||
}
|
||||
|
||||
public static GradientPanel borderPanel(@Nullable final Border border, @Nullable final Color background, final int axis,
|
||||
final Component... components) {
|
||||
GradientPanel box = panel( axis, background, components );
|
||||
if (border != null)
|
||||
box.setBorder( border );
|
||||
|
||||
if (background != null)
|
||||
box.setBackground( background );
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
public static GradientPanel gradientPanel(@Nullable final LayoutManager layout, @Nullable final Color color) {
|
||||
return new GradientPanel( layout, color ) {
|
||||
{
|
||||
setOpaque( color != null );
|
||||
setBackground( null );
|
||||
setAlignmentX( LEFT_ALIGNMENT );
|
||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
||||
}
|
||||
};
|
||||
public static GradientPanel panel(@Nullable final LayoutManager layout) {
|
||||
return panel( layout, null );
|
||||
}
|
||||
|
||||
public static GradientPanel panel(@Nullable final LayoutManager layout, @Nullable final Color color) {
|
||||
return new GradientPanel( layout, color );
|
||||
}
|
||||
|
||||
public static int showDialog(@Nullable final Component owner, @Nullable final String title, final JOptionPane pane) {
|
||||
JDialog dialog = pane.createDialog( owner, title );
|
||||
dialog.setMinimumSize( new Dimension( 520, 0 ) );
|
||||
dialog.setModalityType( Dialog.ModalityType.DOCUMENT_MODAL );
|
||||
showDialog( dialog );
|
||||
|
||||
Object selectedValue = pane.getValue();
|
||||
if (selectedValue == null)
|
||||
return JOptionPane.CLOSED_OPTION;
|
||||
|
||||
Object[] options = pane.getOptions();
|
||||
if (options == null)
|
||||
return (selectedValue instanceof Integer)? (Integer) selectedValue: JOptionPane.CLOSED_OPTION;
|
||||
|
||||
try {
|
||||
int option = Arrays.binarySearch( options, selectedValue );
|
||||
return (option < 0)? JOptionPane.CLOSED_OPTION: option;
|
||||
}
|
||||
catch (final ClassCastException ignored) {
|
||||
return JOptionPane.CLOSED_OPTION;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static File showLoadDialog(@Nullable final Component owner, final String title) {
|
||||
return showFileDialog( owner, title, FileDialog.LOAD, null );
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static File showSaveDialog(@Nullable final Component owner, final String title, final String fileName) {
|
||||
return showFileDialog( owner, title, FileDialog.SAVE, fileName );
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static File showFileDialog(@Nullable final Component owner, final String title,
|
||||
final int mode, @Nullable final String fileName) {
|
||||
FileDialog fileDialog = new FileDialog( JOptionPane.getFrameForComponent( owner ), title, mode );
|
||||
fileDialog.setFile( fileName );
|
||||
fileDialog.setLocationRelativeTo( owner );
|
||||
fileDialog.setLocationByPlatform( true );
|
||||
fileDialog.setVisible( true );
|
||||
|
||||
File[] selectedFiles = fileDialog.getFiles();
|
||||
return ((selectedFiles != null) && (selectedFiles.length > 0))? selectedFiles[0]: null;
|
||||
}
|
||||
|
||||
public static JDialog showDialog(@Nullable final Component owner, @Nullable final String title, final Container content) {
|
||||
JDialog dialog = new JDialog( (owner != null)? SwingUtilities.windowForComponent( owner ): null,
|
||||
title, Dialog.ModalityType.DOCUMENT_MODAL );
|
||||
dialog.setMinimumSize( new Dimension( 320, 0 ) );
|
||||
dialog.setLocationRelativeTo( owner );
|
||||
dialog.setContentPane( content );
|
||||
|
||||
return showDialog( dialog );
|
||||
}
|
||||
|
||||
private static JDialog showDialog(final JDialog dialog) {
|
||||
// OpenJDK does not correctly implement this setting in native code.
|
||||
dialog.getRootPane().putClientProperty( "apple.awt.documentModalSheet", Boolean.TRUE );
|
||||
dialog.getRootPane().putClientProperty( "Window.style", "small" );
|
||||
dialog.pack();
|
||||
|
||||
dialog.setLocationByPlatform( true );
|
||||
dialog.setVisible( true );
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public static JTextField textField() {
|
||||
return new JTextField() {
|
||||
return textField( null );
|
||||
}
|
||||
|
||||
public static JTextField textField(@Nullable final Document document) {
|
||||
return new JTextField( document, null, 0 ) {
|
||||
{
|
||||
setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
|
||||
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) );
|
||||
setFont( Res.valueFont().deriveFont( CONTROL_TEXT_SIZE ) );
|
||||
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
|
||||
setAlignmentX( LEFT_ALIGNMENT );
|
||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getMaximumSize() {
|
||||
return new Dimension( Integer.MAX_VALUE, getPreferredSize().height );
|
||||
return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static JTextField textField(@Nullable final String text, @Nullable final Consumer<String> change) {
|
||||
return textField( new DocumentModel( new PlainDocument() ).selection( text, change ).getDocument() );
|
||||
}
|
||||
|
||||
public static JTextArea textArea() {
|
||||
return new JTextArea() {
|
||||
{
|
||||
setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
|
||||
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) );
|
||||
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
|
||||
setAlignmentX( LEFT_ALIGNMENT );
|
||||
|
||||
setLineWrap( true );
|
||||
setRows( 3 );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getMaximumSize() {
|
||||
return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE );
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -93,7 +216,6 @@ public abstract class Components {
|
||||
setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
|
||||
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) );
|
||||
setAlignmentX( LEFT_ALIGNMENT );
|
||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -103,49 +225,177 @@ public abstract class Components {
|
||||
};
|
||||
}
|
||||
|
||||
public static JButton button(final String label) {
|
||||
return new JButton( label ) {
|
||||
public static <E> JList<E> list(final ListModel<E> model, final Function<E, String> valueTransformer) {
|
||||
return new JList<E>( model ) {
|
||||
{
|
||||
setFont( Res.controlFont().deriveFont( CONTROL_TEXT_SIZE ) );
|
||||
setAlignmentX( LEFT_ALIGNMENT );
|
||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
||||
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
|
||||
setCellRenderer( new DefaultListCellRenderer() {
|
||||
@Override
|
||||
@SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" })
|
||||
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
|
||||
final boolean isSelected, final boolean cellHasFocus) {
|
||||
String label = valueTransformer.apply( (E) value );
|
||||
super.getListCellRendererComponent(
|
||||
list, Strings.isNullOrEmpty( label )? " ": label, index, isSelected, cellHasFocus );
|
||||
setBorder( BorderFactory.createEmptyBorder( 2, 4, 2, 4 ) );
|
||||
|
||||
return this;
|
||||
}
|
||||
} );
|
||||
|
||||
if (model instanceof CollectionListModel)
|
||||
((CollectionListModel<E>) model).registerList( this );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getMaximumSize() {
|
||||
return new Dimension( 20, getPreferredSize().height );
|
||||
return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Component stud() {
|
||||
Dimension studDimension = new Dimension( 8, 8 );
|
||||
public static JScrollPane scrollPane(final Component child) {
|
||||
return new JScrollPane( child ) {
|
||||
{
|
||||
setBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ) );
|
||||
setAlignmentX( LEFT_ALIGNMENT );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static JButton button(final String label, @Nullable final ActionListener actionListener) {
|
||||
return button( new AbstractAction( label ) {
|
||||
@Override
|
||||
public void actionPerformed(final ActionEvent e) {
|
||||
if (actionListener != null)
|
||||
actionListener.actionPerformed( e );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return actionListener != null;
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
public static JButton button(final Icon icon, @Nullable final ActionListener actionListener, @Nullable String toolTip) {
|
||||
JButton iconButton = button( new AbstractAction( null, icon ) {
|
||||
@Override
|
||||
public void actionPerformed(final ActionEvent e) {
|
||||
if (actionListener != null)
|
||||
actionListener.actionPerformed( e );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return actionListener != null;
|
||||
}
|
||||
} );
|
||||
iconButton.setToolTipText( toolTip );
|
||||
iconButton.setFocusable( false );
|
||||
|
||||
return iconButton;
|
||||
}
|
||||
|
||||
public static JButton button(final Action action) {
|
||||
return new JButton( action ) {
|
||||
{
|
||||
setAlignmentX( LEFT_ALIGNMENT );
|
||||
|
||||
if (getText() == null) {
|
||||
setContentAreaFilled( false );
|
||||
setBorderPainted( false );
|
||||
setOpaque( false );
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Component strut() {
|
||||
return strut( SIZE_PADDING );
|
||||
}
|
||||
|
||||
public static Component strut(final int size) {
|
||||
Dimension studDimension = new Dimension( size, size );
|
||||
Box.Filler rigidArea = new Box.Filler( studDimension, studDimension, studDimension );
|
||||
rigidArea.setAlignmentX( Component.LEFT_ALIGNMENT );
|
||||
rigidArea.setAlignmentY( Component.BOTTOM_ALIGNMENT );
|
||||
rigidArea.setBackground( Color.red );
|
||||
return rigidArea;
|
||||
}
|
||||
|
||||
public static int margin() {
|
||||
return SIZE_MARGIN;
|
||||
}
|
||||
|
||||
public static Border marginBorder() {
|
||||
return marginBorder( margin() );
|
||||
}
|
||||
|
||||
public static Border marginBorder(final int size) {
|
||||
return BorderFactory.createEmptyBorder( size, size, size, size );
|
||||
}
|
||||
|
||||
public static JSpinner spinner(final SpinnerModel model) {
|
||||
return new JSpinner( model ) {
|
||||
{
|
||||
CompoundBorder editorBorder = BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
|
||||
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) );
|
||||
DefaultFormatterFactory formatterFactory = new DefaultFormatterFactory();
|
||||
if (model instanceof UnsignedIntegerModel)
|
||||
formatterFactory.setDefaultFormatter( ((UnsignedIntegerModel) model).getFormatter() );
|
||||
((DefaultEditor) getEditor()).getTextField().setFormatterFactory( formatterFactory );
|
||||
((DefaultEditor) getEditor()).getTextField().setBorder( editorBorder );
|
||||
setAlignmentX( LEFT_ALIGNMENT );
|
||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
||||
setBorder( null );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static JLabel heading() {
|
||||
return heading( " " );
|
||||
}
|
||||
|
||||
public static JLabel heading(final int horizontalAlignment) {
|
||||
return heading( " ", horizontalAlignment );
|
||||
}
|
||||
|
||||
public static JLabel heading(@Nullable final String heading) {
|
||||
return heading( heading, SwingConstants.CENTER );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param horizontalAlignment One of the following constants
|
||||
* defined in {@code SwingConstants}:
|
||||
* {@code LEFT},
|
||||
* {@code CENTER},
|
||||
* {@code RIGHT},
|
||||
* {@code LEADING} or
|
||||
* {@code TRAILING}.
|
||||
*/
|
||||
public static JLabel heading(@Nullable final String heading, final int horizontalAlignment) {
|
||||
return new JLabel( heading, horizontalAlignment ) {
|
||||
{
|
||||
setFont( getFont().deriveFont( Font.BOLD, TEXT_SIZE_HEADING ) );
|
||||
setAlignmentX( LEFT_ALIGNMENT );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getMaximumSize() {
|
||||
return new Dimension( 20, getPreferredSize().height );
|
||||
return new Dimension( Integer.MAX_VALUE, getPreferredSize().height );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static JLabel label() {
|
||||
return label( " " );
|
||||
}
|
||||
|
||||
public static JLabel label(final int horizontalAlignment) {
|
||||
return label( " ", horizontalAlignment );
|
||||
}
|
||||
|
||||
public static JLabel label(@Nullable final String label) {
|
||||
return label( label, SwingConstants.LEADING );
|
||||
}
|
||||
@@ -162,9 +412,7 @@ public abstract class Components {
|
||||
public static JLabel label(@Nullable final String label, final int horizontalAlignment) {
|
||||
return new JLabel( label, horizontalAlignment ) {
|
||||
{
|
||||
setFont( Res.controlFont().deriveFont( CONTROL_TEXT_SIZE ) );
|
||||
setAlignmentX( LEFT_ALIGNMENT );
|
||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -177,30 +425,52 @@ public abstract class Components {
|
||||
public static JCheckBox checkBox(final String label) {
|
||||
return new JCheckBox( label ) {
|
||||
{
|
||||
setFont( Res.controlFont().deriveFont( CONTROL_TEXT_SIZE ) );
|
||||
setBackground( null );
|
||||
setAlignmentX( LEFT_ALIGNMENT );
|
||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <V> JComboBox<V> comboBox(final V... values) {
|
||||
return comboBox( new DefaultComboBoxModel<>( values ) );
|
||||
public static <E> JComboBox<E> comboBox(final Function<E, String> valueTransformer, final E... values) {
|
||||
return comboBox( new DefaultComboBoxModel<>( values ), valueTransformer );
|
||||
}
|
||||
|
||||
public static <M> JComboBox<M> comboBox(final ComboBoxModel<M> model) {
|
||||
return new JComboBox<M>( model ) {
|
||||
public static <E> JComboBox<E> comboBox(final E[] values, final Function<E, String> valueTransformer,
|
||||
@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
|
||||
return comboBox( new CollectionListModel<>( values ).selection( selectedItem, selectionConsumer ), valueTransformer );
|
||||
}
|
||||
|
||||
public static <E> JComboBox<E> comboBox(final Collection<E> values, final Function<E, String> valueTransformer,
|
||||
@Nullable final Consumer<E> selectionConsumer) {
|
||||
return comboBox( new CollectionListModel<>( values ).selection( selectionConsumer ), valueTransformer );
|
||||
}
|
||||
|
||||
public static <E> JComboBox<E> comboBox(final Collection<E> values, final Function<E, String> valueTransformer,
|
||||
@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
|
||||
return comboBox( new CollectionListModel<>( values ).selection( selectedItem, selectionConsumer ), valueTransformer );
|
||||
}
|
||||
|
||||
public static <E> JComboBox<E> comboBox(final ComboBoxModel<E> model, final Function<E, String> valueTransformer) {
|
||||
return new JComboBox<E>( model ) {
|
||||
{
|
||||
// CompoundBorder editorBorder = BorderFactory.createCompoundBorder(
|
||||
// BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
|
||||
// BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) );
|
||||
// ((JComponent) ((BasicComboBoxEditor) getEditor()).getEditorComponent()).setBorder(editorBorder);
|
||||
setFont( Res.controlFont().deriveFont( CONTROL_TEXT_SIZE ) );
|
||||
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
|
||||
setBorder( BorderFactory.createEmptyBorder( 4, 0, 4, 0 ) );
|
||||
setRenderer( new DefaultListCellRenderer() {
|
||||
@Override
|
||||
@SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" })
|
||||
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
|
||||
final boolean isSelected, final boolean cellHasFocus) {
|
||||
String label = valueTransformer.apply( (E) value );
|
||||
super.getListCellRendererComponent(
|
||||
list, Strings.isNullOrEmpty( label )? " ": label, index, isSelected, cellHasFocus );
|
||||
setBorder( BorderFactory.createEmptyBorder( 0, 4, 0, 4 ) );
|
||||
|
||||
return this;
|
||||
}
|
||||
} );
|
||||
putClientProperty( "JComboBox.isPopDown", Boolean.TRUE );
|
||||
setAlignmentX( LEFT_ALIGNMENT );
|
||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
||||
// setBorder(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -210,6 +480,24 @@ public abstract class Components {
|
||||
};
|
||||
}
|
||||
|
||||
public static JEditorPane linkLabel(@NonNls final String html) {
|
||||
return new JEditorPane( "text/html", "<html><body style='width:640;font-family:sans-serif'>" + html ) {
|
||||
{
|
||||
setOpaque( false );
|
||||
setEditable( false );
|
||||
addHyperlinkListener( event -> {
|
||||
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED)
|
||||
try {
|
||||
Platform.get().open( event.getURL().toURI() );
|
||||
}
|
||||
catch (final URISyntaxException e) {
|
||||
logger.err( e, "After triggering hyperlink: %s", event );
|
||||
}
|
||||
} );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static class GradientPanel extends JPanel {
|
||||
|
||||
@Nullable
|
||||
@@ -218,10 +506,26 @@ public abstract class Components {
|
||||
@Nullable
|
||||
private GradientPaint paint;
|
||||
|
||||
protected GradientPanel(@Nullable final LayoutManager layout, @Nullable final Color gradientColor) {
|
||||
public GradientPanel() {
|
||||
this( null, null );
|
||||
}
|
||||
|
||||
public GradientPanel(@Nullable final Color gradientColor) {
|
||||
this( null, gradientColor );
|
||||
}
|
||||
|
||||
public GradientPanel(@Nullable final LayoutManager layout) {
|
||||
this( layout, null );
|
||||
}
|
||||
|
||||
public GradientPanel(@Nullable final LayoutManager layout, @Nullable final Color gradientColor) {
|
||||
super( layout );
|
||||
this.gradientColor = gradientColor;
|
||||
if (getLayout() == null)
|
||||
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
|
||||
|
||||
setGradientColor( gradientColor );
|
||||
setBackground( null );
|
||||
setAlignmentX( LEFT_ALIGNMENT );
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -231,17 +535,30 @@ public abstract class Components {
|
||||
|
||||
public void setGradientColor(@Nullable final Color gradientColor) {
|
||||
this.gradientColor = gradientColor;
|
||||
revalidate();
|
||||
updatePaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doLayout() {
|
||||
super.doLayout();
|
||||
public void setBackground(@Nullable final Color bg) {
|
||||
super.setBackground( bg );
|
||||
setOpaque( bg != null );
|
||||
}
|
||||
|
||||
if (gradientColor != null) {
|
||||
paint = new GradientPaint( new Point( 0, 0 ), gradientColor, new Point( getWidth(), getHeight() ), gradientColor.darker() );
|
||||
repaint();
|
||||
@Override
|
||||
public void setBounds(final int x, final int y, final int width, final int height) {
|
||||
super.setBounds( x, y, width, height );
|
||||
updatePaint();
|
||||
}
|
||||
|
||||
private void updatePaint() {
|
||||
if (gradientColor == null) {
|
||||
paint = null;
|
||||
return;
|
||||
}
|
||||
|
||||
paint = new GradientPaint( new Point( 0, 0 ), gradientColor,
|
||||
new Point( getWidth(), getHeight() ), gradientColor.darker() );
|
||||
repaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.lyndir.masterpassword.gui.util;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-08-23
|
||||
*/
|
||||
public class ConsumingTrigger<T> implements Consumer<T> {
|
||||
|
||||
private final Runnable trigger;
|
||||
|
||||
@Nullable
|
||||
private T value;
|
||||
|
||||
public ConsumingTrigger(final Runnable trigger) {
|
||||
this.trigger = trigger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(final T t) {
|
||||
value = t;
|
||||
|
||||
trigger.run();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.lyndir.masterpassword.gui.util;
|
||||
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import java.util.function.Consumer;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import javax.swing.text.BadLocationException;
|
||||
import javax.swing.text.Document;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-08-24
|
||||
*/
|
||||
public class DocumentModel implements Selectable<String, DocumentModel> {
|
||||
|
||||
private static final Logger logger = Logger.get( DocumentModel.class );
|
||||
|
||||
private final Document document;
|
||||
|
||||
@Nullable
|
||||
private DocumentListener documentListener;
|
||||
|
||||
public DocumentModel(final Document document) {
|
||||
this.document = document;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Document getDocument() {
|
||||
return document;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getText() {
|
||||
try {
|
||||
return (document.getLength() > 0)? document.getText( 0, document.getLength() ): null;
|
||||
}
|
||||
catch (final BadLocationException e) {
|
||||
logger.wrn( "While getting text for model", e );
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setText(@Nullable final String text) {
|
||||
try {
|
||||
if (document.getLength() > 0)
|
||||
document.remove( 0, document.getLength() );
|
||||
|
||||
if (text != null)
|
||||
document.insertString( 0, text, null );
|
||||
}
|
||||
catch (final BadLocationException e) {
|
||||
logger.err( "While setting text for model", e );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentModel selection(@Nullable final Consumer<String> selectionConsumer) {
|
||||
if (documentListener != null)
|
||||
document.removeDocumentListener( documentListener );
|
||||
|
||||
if (selectionConsumer != null)
|
||||
document.addDocumentListener( documentListener = new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(final DocumentEvent e) {
|
||||
trigger();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(final DocumentEvent e) {
|
||||
trigger();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(final DocumentEvent e) {
|
||||
trigger();
|
||||
}
|
||||
|
||||
private void trigger() {
|
||||
selectionConsumer.accept( getText() );
|
||||
}
|
||||
} );
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentModel selection(@Nullable final String selectedItem, @Nullable final Consumer<String> selectionConsumer) {
|
||||
selection( selectionConsumer );
|
||||
setText( selectedItem );
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.lyndir.masterpassword.gui.util;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-08
|
||||
*/
|
||||
public abstract class FailableCallback<T> implements FutureCallback<T> {
|
||||
|
||||
private final Logger logger;
|
||||
|
||||
protected FailableCallback(final Logger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(final Throwable t) {
|
||||
logger.err( t, "Future failed." );
|
||||
onSuccess( null );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.lyndir.masterpassword.gui.util;
|
||||
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import com.lyndir.lhunath.opal.system.util.TypeUtils;
|
||||
import com.lyndir.masterpassword.gui.util.platform.BasePlatform;
|
||||
import com.lyndir.masterpassword.gui.util.platform.IPlatform;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-29
|
||||
*/
|
||||
public final class Platform {
|
||||
|
||||
private static final Logger logger = Logger.get( Platform.class );
|
||||
private static final IPlatform activePlatform;
|
||||
|
||||
static {
|
||||
IPlatform tryPlatform;
|
||||
if (null != (tryPlatform = construct( "com.lyndir.masterpassword.gui.util.platform.JDK9Platform" )))
|
||||
activePlatform = tryPlatform;
|
||||
|
||||
else if (null != (tryPlatform = construct( "com.lyndir.masterpassword.gui.util.platform.ApplePlatform" )))
|
||||
activePlatform = tryPlatform;
|
||||
|
||||
else
|
||||
activePlatform = new BasePlatform();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static <T> T construct(final String typeName) {
|
||||
try {
|
||||
// AppleGUI adds support for macOS features.
|
||||
Optional<Class<T>> gui = TypeUtils.loadClass( typeName );
|
||||
if (gui.isPresent())
|
||||
return gui.get().getConstructor().newInstance();
|
||||
}
|
||||
catch (@SuppressWarnings("ErrorNotRethrown") final LinkageError ignored) {
|
||||
}
|
||||
catch (final IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
|
||||
throw logger.bug( e );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static IPlatform get() {
|
||||
return activePlatform;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
//==============================================================================
|
||||
// This file is part of Master Password.
|
||||
// Copyright (c) 2011-2017, Maarten Billemont.
|
||||
//
|
||||
// Master Password is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Master Password is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You can find a copy of the GNU General Public License in the
|
||||
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
|
||||
//==============================================================================
|
||||
|
||||
package com.lyndir.masterpassword.gui.util;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.common.util.concurrent.*;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import com.lyndir.masterpassword.MPIdenticon;
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Consumer;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.*;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
import org.joda.time.*;
|
||||
import org.joda.time.format.DateTimeFormat;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2014-06-11
|
||||
*/
|
||||
@SuppressWarnings({ "HardcodedFileSeparator", "MethodReturnAlwaysConstant", "SpellCheckingInspection", "serial" })
|
||||
public abstract class Res {
|
||||
|
||||
private static final int AVATAR_COUNT = 19;
|
||||
private static final ListeningScheduledExecutorService jobExecutor = MoreExecutors.listeningDecorator(
|
||||
Executors.newSingleThreadScheduledExecutor() );
|
||||
private static final Executor immediateUiExecutor = new SwingExecutorService( true );
|
||||
private static final Executor laterUiExecutor = new SwingExecutorService( false );
|
||||
private static final Logger logger = Logger.get( Res.class );
|
||||
private static final Icons icons = new Icons();
|
||||
private static final Fonts fonts = new Fonts();
|
||||
private static final Colors colors = new Colors();
|
||||
|
||||
public static Future<?> job(final Runnable job) {
|
||||
return job( job, 0, TimeUnit.MILLISECONDS );
|
||||
}
|
||||
|
||||
public static Future<?> job(final Runnable job, final long delay, final TimeUnit timeUnit) {
|
||||
return jobExecutor.schedule( () -> {
|
||||
try {
|
||||
job.run();
|
||||
}
|
||||
catch (final Throwable t) {
|
||||
logger.err( t, "Unexpected: %s", t.getLocalizedMessage() );
|
||||
}
|
||||
}, delay, timeUnit );
|
||||
}
|
||||
|
||||
public static <V> ListenableFuture<V> job(final Callable<V> job) {
|
||||
return job( job, 0, TimeUnit.MILLISECONDS );
|
||||
}
|
||||
|
||||
public static <V> void job(final Callable<V> job, final Consumer<V> callback) {
|
||||
Futures.addCallback( job( job, 0, TimeUnit.MILLISECONDS ), new FailableCallback<V>( logger ) {
|
||||
@Override
|
||||
public void onSuccess(@Nullable final V result) {
|
||||
callback.accept( result );
|
||||
}
|
||||
}, uiExecutor() );
|
||||
}
|
||||
|
||||
public static <V> ListenableFuture<V> job(final Callable<V> job, final long delay, final TimeUnit timeUnit) {
|
||||
return jobExecutor.schedule( job, delay, timeUnit );
|
||||
}
|
||||
|
||||
public static void ui(final Runnable job) {
|
||||
ui( true, job );
|
||||
}
|
||||
|
||||
public static void ui(final boolean immediate, final Runnable job) {
|
||||
uiExecutor( immediate ).execute( job );
|
||||
}
|
||||
|
||||
public static Executor uiExecutor() {
|
||||
return uiExecutor( true );
|
||||
}
|
||||
|
||||
public static Executor uiExecutor(final boolean immediate) {
|
||||
return immediate? immediateUiExecutor: laterUiExecutor;
|
||||
}
|
||||
|
||||
public static Icons icons() {
|
||||
return icons;
|
||||
}
|
||||
|
||||
public static Fonts fonts() {
|
||||
return fonts;
|
||||
}
|
||||
|
||||
public static Colors colors() {
|
||||
return colors;
|
||||
}
|
||||
|
||||
public static String format(final ReadableInstant instant) {
|
||||
return DateTimeFormat.mediumDateTime().print( new DateTime( instant, DateTimeZone.getDefault() ) );
|
||||
}
|
||||
|
||||
public static final class Icons {
|
||||
|
||||
public Icon add() {
|
||||
return icon( "media/icon_add.png" );
|
||||
}
|
||||
|
||||
public Icon delete() {
|
||||
return icon( "media/icon_delete.png" );
|
||||
}
|
||||
|
||||
public Icon question() {
|
||||
return icon( "media/icon_question.png" );
|
||||
}
|
||||
|
||||
public Icon user() {
|
||||
return icon( "media/icon_user.png" );
|
||||
}
|
||||
|
||||
public Icon lock() {
|
||||
return icon( "media/icon_lock.png" );
|
||||
}
|
||||
|
||||
public Icon reset() {
|
||||
return icon( "media/icon_reset.png" );
|
||||
}
|
||||
|
||||
public Icon import_() {
|
||||
return icon( "media/icon_import.png" );
|
||||
}
|
||||
|
||||
public Icon help() {
|
||||
return icon( "media/icon_help.png" );
|
||||
}
|
||||
|
||||
public Icon export() {
|
||||
return icon( "media/icon_export.png" );
|
||||
}
|
||||
|
||||
public Icon settings() {
|
||||
return icon( "media/icon_settings.png" );
|
||||
}
|
||||
|
||||
public Icon edit() {
|
||||
return icon( "media/icon_edit.png" );
|
||||
}
|
||||
|
||||
public Icon key() {
|
||||
return icon( "media/icon_key.png" );
|
||||
}
|
||||
|
||||
public Icon avatar(final int index) {
|
||||
return icon( strf( "media/avatar-%d.png", index % avatars() ) );
|
||||
}
|
||||
|
||||
public int avatars() {
|
||||
return AVATAR_COUNT;
|
||||
}
|
||||
|
||||
private static Icon icon(@NonNls final String resourceName) {
|
||||
return new ImageIcon( Toolkit.getDefaultToolkit().getImage( Res.class.getClassLoader().getResource( resourceName ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static final class Fonts {
|
||||
|
||||
public Font emoticonsFont(final float size) {
|
||||
return emoticonsRegular().deriveFont( size );
|
||||
}
|
||||
|
||||
public Font controlFont(final float size) {
|
||||
return exoRegular().deriveFont( size );
|
||||
}
|
||||
|
||||
public Font valueFont(final float size) {
|
||||
return sourceSansProRegular().deriveFont( size );
|
||||
}
|
||||
|
||||
public Font bigValueFont(final float size) {
|
||||
return sourceSansProBlack().deriveFont( size );
|
||||
}
|
||||
|
||||
public Font emoticonsRegular() {
|
||||
return font( "fonts/Emoticons-Regular.otf" );
|
||||
}
|
||||
|
||||
public Font sourceCodeProRegular() {
|
||||
return font( "fonts/SourceCodePro-Regular.otf" );
|
||||
}
|
||||
|
||||
public Font sourceCodeProBlack() {
|
||||
return font( "fonts/SourceCodePro-Bold.otf" );
|
||||
}
|
||||
|
||||
public Font sourceSansProRegular() {
|
||||
return font( "fonts/SourceSansPro-Regular.otf" );
|
||||
}
|
||||
|
||||
public Font sourceSansProBlack() {
|
||||
return font( "fonts/SourceSansPro-Bold.otf" );
|
||||
}
|
||||
|
||||
public Font exoBold() {
|
||||
return font( "fonts/Exo2.0-Bold.otf" );
|
||||
}
|
||||
|
||||
public Font exoExtraBold() {
|
||||
return font( "fonts/Exo2.0-ExtraBold.otf" );
|
||||
}
|
||||
|
||||
public Font exoRegular() {
|
||||
return font( "fonts/Exo2.0-Regular.otf" );
|
||||
}
|
||||
|
||||
public Font exoThin() {
|
||||
return font( "fonts/Exo2.0-Thin.otf" );
|
||||
}
|
||||
|
||||
public Font arimoBold() {
|
||||
return font( "fonts/Arimo-Bold.ttf" );
|
||||
}
|
||||
|
||||
public Font arimoBoldItalic() {
|
||||
return font( "fonts/Arimo-BoldItalic.ttf" );
|
||||
}
|
||||
|
||||
public Font arimoItalic() {
|
||||
return font( "fonts/Arimo-Italic.ttf" );
|
||||
}
|
||||
|
||||
public Font arimoRegular() {
|
||||
return font( "fonts/Arimo-Regular.ttf" );
|
||||
}
|
||||
|
||||
private static Font font(@NonNls final String fontResourceName) {
|
||||
Map<String, SoftReference<Font>> fontsByResourceName = Maps.newHashMap();
|
||||
SoftReference<Font> fontRef = fontsByResourceName.get( fontResourceName );
|
||||
Font font = (fontRef == null)? null: fontRef.get();
|
||||
if (font == null)
|
||||
try {
|
||||
fontsByResourceName.put( fontResourceName, new SoftReference<>(
|
||||
font = Font.createFont( Font.TRUETYPE_FONT, Resources.getResource( fontResourceName ).openStream() ) ) );
|
||||
}
|
||||
catch (final FontFormatException | IOException e) {
|
||||
throw logger.bug( e );
|
||||
}
|
||||
|
||||
return font;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static final class Colors {
|
||||
|
||||
private final Color transparent = new Color( 0, 0, 0, 0 );
|
||||
private final Color frameBg = Color.decode( "#5A5D6B" );
|
||||
private final Color controlBg = SystemColor.window;
|
||||
private final Color controlBorder = Color.decode( "#BFBFBF" );
|
||||
private final Color highlightFg = SystemColor.controlHighlight;
|
||||
private final Color errorFg = Color.decode( "#FF3333" );
|
||||
|
||||
public Color transparent() {
|
||||
return transparent;
|
||||
}
|
||||
|
||||
public Color frameBg() {
|
||||
return frameBg;
|
||||
}
|
||||
|
||||
public Color controlBg() {
|
||||
return controlBg;
|
||||
}
|
||||
|
||||
public Color controlBorder() {
|
||||
return controlBorder;
|
||||
}
|
||||
|
||||
public Color highlightFg() {
|
||||
return highlightFg;
|
||||
}
|
||||
|
||||
public Color errorFg() {
|
||||
return errorFg;
|
||||
}
|
||||
|
||||
public Color fromIdenticonColor(final MPIdenticon.Color identiconColor, final BackgroundMode backgroundMode) {
|
||||
switch (identiconColor) {
|
||||
case RED:
|
||||
switch (backgroundMode) {
|
||||
case DARK:
|
||||
return Color.decode( "#dc322f" );
|
||||
case LIGHT:
|
||||
return Color.decode( "#dc322f" );
|
||||
}
|
||||
break;
|
||||
case GREEN:
|
||||
switch (backgroundMode) {
|
||||
case DARK:
|
||||
return Color.decode( "#859900" );
|
||||
case LIGHT:
|
||||
return Color.decode( "#859900" );
|
||||
}
|
||||
break;
|
||||
case YELLOW:
|
||||
switch (backgroundMode) {
|
||||
case DARK:
|
||||
return Color.decode( "#b58900" );
|
||||
case LIGHT:
|
||||
return Color.decode( "#b58900" );
|
||||
}
|
||||
break;
|
||||
case BLUE:
|
||||
switch (backgroundMode) {
|
||||
case DARK:
|
||||
return Color.decode( "#268bd2" );
|
||||
case LIGHT:
|
||||
return Color.decode( "#268bd2" );
|
||||
}
|
||||
break;
|
||||
case MAGENTA:
|
||||
switch (backgroundMode) {
|
||||
case DARK:
|
||||
return Color.decode( "#d33682" );
|
||||
case LIGHT:
|
||||
return Color.decode( "#d33682" );
|
||||
}
|
||||
break;
|
||||
case CYAN:
|
||||
switch (backgroundMode) {
|
||||
case DARK:
|
||||
return Color.decode( "#2aa198" );
|
||||
case LIGHT:
|
||||
return Color.decode( "#2aa198" );
|
||||
}
|
||||
break;
|
||||
case MONO:
|
||||
switch (backgroundMode) {
|
||||
case DARK:
|
||||
return Color.decode( "#93a1a1" );
|
||||
case LIGHT:
|
||||
return Color.decode( "#586e75" );
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException( strf( "Color: %s or mode: %s not supported: ", identiconColor, backgroundMode ) );
|
||||
}
|
||||
|
||||
public enum BackgroundMode {
|
||||
DARK, LIGHT
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.lyndir.masterpassword.gui.util;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-26
|
||||
*/
|
||||
public interface Selectable<E, T> {
|
||||
|
||||
T selection(@Nullable Consumer<E> selectionConsumer);
|
||||
|
||||
T selection(@Nullable E selectedItem, @Nullable Consumer<E> selectionConsumer);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.lyndir.masterpassword.gui.util;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
||||
|
||||
import com.google.common.collect.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
import javax.swing.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-08
|
||||
*/
|
||||
public class SwingExecutorService extends AbstractExecutorService {
|
||||
|
||||
private final List<Runnable> pendingCommands = Lists.newLinkedList();
|
||||
private final BlockingQueue<Boolean> terminated = Queues.newLinkedBlockingDeque( 1 );
|
||||
private final boolean immediate;
|
||||
private boolean shutdown;
|
||||
|
||||
/**
|
||||
* @param immediate Allow immediate execution of the job in {@link #execute(Runnable)} if already on the right thread.
|
||||
* If {@code false}, jobs are always posted for later execution on the event thread.
|
||||
*/
|
||||
public SwingExecutorService(final boolean immediate) {
|
||||
this.immediate = immediate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
synchronized (pendingCommands) {
|
||||
shutdown = true;
|
||||
|
||||
if (pendingCommands.isEmpty())
|
||||
terminated.add( true );
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<Runnable> shutdownNow() {
|
||||
shutdown();
|
||||
|
||||
synchronized (pendingCommands) {
|
||||
return ImmutableList.copyOf( pendingCommands );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShutdown() {
|
||||
synchronized (pendingCommands) {
|
||||
return shutdown;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
return ifNotNullElse( terminated.peek(), false );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitTermination(final long timeout, @NotNull final TimeUnit unit)
|
||||
throws InterruptedException {
|
||||
return ifNotNullElse( terminated.poll( timeout, unit ), false );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(@NotNull final Runnable command) {
|
||||
synchronized (pendingCommands) {
|
||||
if (shutdown)
|
||||
throw new RejectedExecutionException( "Executor is shut down." );
|
||||
|
||||
pendingCommands.add( command );
|
||||
}
|
||||
|
||||
if (immediate && SwingUtilities.isEventDispatchThread())
|
||||
run( command );
|
||||
else
|
||||
SwingUtilities.invokeLater( () -> run( command ) );
|
||||
}
|
||||
|
||||
private void run(final Runnable command) {
|
||||
command.run();
|
||||
|
||||
synchronized (pendingCommands) {
|
||||
pendingCommands.remove( command );
|
||||
|
||||
if (shutdown && pendingCommands.isEmpty())
|
||||
terminated.add( true );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,15 +19,21 @@
|
||||
package com.lyndir.masterpassword.gui.util;
|
||||
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
import java.text.ParseException;
|
||||
import java.util.function.Consumer;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeListener;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2016-10-29
|
||||
*/
|
||||
public class UnsignedIntegerModel extends SpinnerNumberModel {
|
||||
@SuppressWarnings("serial")
|
||||
public class UnsignedIntegerModel extends SpinnerNumberModel implements Selectable<UnsignedInteger, UnsignedIntegerModel> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Nullable
|
||||
private ChangeListener changeListener;
|
||||
|
||||
public UnsignedIntegerModel() {
|
||||
this( UnsignedInteger.ZERO, UnsignedInteger.ZERO, UnsignedInteger.MAX_VALUE, UnsignedInteger.ONE );
|
||||
@@ -55,4 +61,80 @@ public class UnsignedIntegerModel extends SpinnerNumberModel {
|
||||
public UnsignedInteger getNumber() {
|
||||
return (UnsignedInteger) super.getNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnsignedInteger getMinimum() {
|
||||
return (UnsignedInteger) super.getMinimum();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnsignedInteger getMaximum() {
|
||||
return (UnsignedInteger) super.getMaximum();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnsignedInteger getStepSize() {
|
||||
return (UnsignedInteger) super.getStepSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnsignedInteger getNextValue() {
|
||||
if ((getMaximum() == null) || (getMaximum().compareTo( getNumber() ) > 0))
|
||||
return getNumber().plus( getStepSize() );
|
||||
|
||||
return getMaximum();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnsignedInteger getPreviousValue() {
|
||||
if ((getMinimum() == null) || (getMinimum().compareTo( getNumber() ) < 0))
|
||||
return getNumber().minus( getStepSize() );
|
||||
|
||||
return getMinimum();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnsignedIntegerModel selection(@Nullable final Consumer<UnsignedInteger> selectionConsumer) {
|
||||
if (changeListener != null) {
|
||||
removeChangeListener( changeListener );
|
||||
changeListener = null;
|
||||
}
|
||||
|
||||
if (selectionConsumer != null) {
|
||||
addChangeListener( changeListener = e -> selectionConsumer.accept( getNumber() ) );
|
||||
selectionConsumer.accept( getNumber() );
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnsignedIntegerModel selection(@Nullable final UnsignedInteger selectedItem,
|
||||
@Nullable final Consumer<UnsignedInteger> selectionConsumer) {
|
||||
if (changeListener != null) {
|
||||
removeChangeListener( changeListener );
|
||||
changeListener = null;
|
||||
}
|
||||
|
||||
setValue( (selectedItem != null)? selectedItem: getMinimum() );
|
||||
return selection( selectionConsumer );
|
||||
}
|
||||
|
||||
public JFormattedTextField.AbstractFormatter getFormatter() {
|
||||
return new JFormattedTextField.AbstractFormatter() {
|
||||
@Override
|
||||
@Nullable
|
||||
public Object stringToValue(@Nullable final String text)
|
||||
throws ParseException {
|
||||
return (text != null)? UnsignedInteger.valueOf( text ): null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String valueToString(final Object value)
|
||||
throws ParseException {
|
||||
return (value != null)? value.toString(): null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.lyndir.masterpassword.gui.util.platform;
|
||||
|
||||
import com.apple.eawt.*;
|
||||
import com.apple.eio.FileManager;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import java.io.*;
|
||||
import java.net.URI;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-29
|
||||
*/
|
||||
public class ApplePlatform implements IPlatform {
|
||||
|
||||
private static final Logger logger = Logger.get( ApplePlatform.class );
|
||||
private static final Application application = Preconditions.checkNotNull(
|
||||
Application.getApplication(), "Not an Apple Java application." );
|
||||
|
||||
@Override
|
||||
public boolean installAppForegroundHandler(final Runnable handler) {
|
||||
application.addAppEventListener( new AppForegroundListener() {
|
||||
@Override
|
||||
public void appMovedToBackground(final AppEvent.AppForegroundEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appRaisedToForeground(final AppEvent.AppForegroundEvent e) {
|
||||
handler.run();
|
||||
}
|
||||
} );
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean installAppReopenHandler(final Runnable handler) {
|
||||
application.addAppEventListener( (AppReOpenedListener) e -> handler.run() );
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requestForeground() {
|
||||
application.requestForeground( true );
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean show(final File file) {
|
||||
try {
|
||||
return FileManager.revealInFinder( file );
|
||||
}
|
||||
catch (final FileNotFoundException e) {
|
||||
logger.err( e, "While showing: %s", file );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean open(final URI url) {
|
||||
try {
|
||||
FileManager.openURL( url.toString() );
|
||||
return true;
|
||||
}
|
||||
catch (final IOException e) {
|
||||
logger.err( e, "While opening: %s", url );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.lyndir.masterpassword.gui.util.platform;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-29
|
||||
*/
|
||||
public class BasePlatform implements IPlatform {
|
||||
|
||||
@Override
|
||||
public boolean installAppForegroundHandler(final Runnable handler) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean installAppReopenHandler(final Runnable handler) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requestForeground() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean show(final File file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean open(final URI url) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.lyndir.masterpassword.gui.util.platform;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-29
|
||||
*/
|
||||
public interface IPlatform {
|
||||
|
||||
boolean installAppForegroundHandler(Runnable handler);
|
||||
|
||||
boolean installAppReopenHandler(Runnable handler);
|
||||
|
||||
boolean requestForeground();
|
||||
|
||||
boolean show(File file);
|
||||
|
||||
boolean open(URI url);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.lyndir.masterpassword.gui.util.platform;
|
||||
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import java.awt.*;
|
||||
import java.awt.desktop.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-29
|
||||
*/
|
||||
@SuppressWarnings("Since15")
|
||||
public class JDK9Platform implements IPlatform {
|
||||
|
||||
private static final Logger logger = Logger.get( JDK9Platform.class );
|
||||
private static final Desktop desktop = Desktop.getDesktop();
|
||||
|
||||
@Override
|
||||
public boolean installAppForegroundHandler(final Runnable handler) {
|
||||
desktop.addAppEventListener( new AppForegroundListener() {
|
||||
@Override
|
||||
public void appRaisedToForeground(final AppForegroundEvent e) {
|
||||
handler.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appMovedToBackground(final AppForegroundEvent e) {
|
||||
}
|
||||
} );
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean installAppReopenHandler(final Runnable handler) {
|
||||
desktop.addAppEventListener( (AppReopenedListener) e -> handler.run() );
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requestForeground() {
|
||||
desktop.requestForeground( true );
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean show(final File file) {
|
||||
if (!file.exists())
|
||||
return false;
|
||||
|
||||
desktop.browseFileDirectory( file );
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean open(final URI url) {
|
||||
try {
|
||||
desktop.browse( url );
|
||||
return true;
|
||||
}
|
||||
catch (final IOException e) {
|
||||
logger.err( e, "While opening: %s", url );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,6 @@
|
||||
* @author lhunath, 2018-04-26
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
package com.lyndir.masterpassword.gui.platform.mac;
|
||||
package com.lyndir.masterpassword.gui.util.platform;
|
||||
|
||||
import javax.annotation.ParametersAreNonnullByDefault;
|
||||
@@ -1,84 +0,0 @@
|
||||
//==============================================================================
|
||||
// This file is part of Master Password.
|
||||
// Copyright (c) 2011-2017, Maarten Billemont.
|
||||
//
|
||||
// Master Password is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Master Password is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You can find a copy of the GNU General Public License in the
|
||||
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
|
||||
//==============================================================================
|
||||
|
||||
package com.lyndir.masterpassword.gui.view;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.lyndir.masterpassword.gui.Res;
|
||||
import com.lyndir.masterpassword.gui.util.Components;
|
||||
import com.lyndir.masterpassword.model.MPUser;
|
||||
import java.awt.*;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.*;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2014-06-11
|
||||
*/
|
||||
public abstract class AuthenticationPanel<U extends MPUser<?>> extends Components.GradientPanel {
|
||||
|
||||
protected final UnlockFrame unlockFrame;
|
||||
protected final JLabel avatarLabel;
|
||||
|
||||
protected AuthenticationPanel(final UnlockFrame unlockFrame) {
|
||||
super( null, null );
|
||||
this.unlockFrame = unlockFrame;
|
||||
|
||||
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
|
||||
|
||||
// Avatar
|
||||
add( Box.createVerticalGlue() );
|
||||
add( avatarLabel = new JLabel( Res.avatar( 0 ) ) {
|
||||
@Override
|
||||
public Dimension getMaximumSize() {
|
||||
return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE );
|
||||
}
|
||||
} );
|
||||
add( Box.createVerticalGlue() );
|
||||
|
||||
avatarLabel.setToolTipText( "The avatar for your user. Click to change it." );
|
||||
}
|
||||
|
||||
protected void updateUser(final boolean repack) {
|
||||
unlockFrame.updateUser( getSelectedUser() );
|
||||
validate();
|
||||
|
||||
if (repack)
|
||||
unlockFrame.repack();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected abstract U getSelectedUser();
|
||||
|
||||
@Nonnull
|
||||
public abstract char[] getMasterPassword();
|
||||
|
||||
@Nullable
|
||||
public Component getFocusComponent() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public Iterable<? extends JButton> getButtons() {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
public abstract void reset();
|
||||
|
||||
public abstract PasswordFrame<?, ?> newPasswordFrame();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.lyndir.masterpassword.gui.view;
|
||||
|
||||
import static com.lyndir.masterpassword.util.Utilities.*;
|
||||
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
import com.lyndir.masterpassword.gui.MasterPassword;
|
||||
import com.lyndir.masterpassword.gui.util.*;
|
||||
import com.lyndir.masterpassword.model.MPUser;
|
||||
import com.lyndir.masterpassword.model.impl.MPFileUser;
|
||||
import com.lyndir.masterpassword.model.impl.MPFileUserManager;
|
||||
import java.awt.*;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.*;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-14
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class FilesPanel extends JPanel implements MPFileUserManager.Listener, MasterPassword.Listener {
|
||||
|
||||
private final JButton avatarButton = Components.button( Res.icons().avatar( 0 ), event -> setAvatar(),
|
||||
"Click to change the user's avatar." );
|
||||
|
||||
private final CollectionListModel<MPUser<?>> usersModel =
|
||||
new CollectionListModel<MPUser<?>>( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser );
|
||||
|
||||
protected FilesPanel() {
|
||||
setOpaque( false );
|
||||
setBackground( Res.colors().transparent() );
|
||||
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
|
||||
|
||||
// -
|
||||
add( Box.createVerticalGlue() );
|
||||
|
||||
// Avatar
|
||||
add( avatarButton );
|
||||
avatarButton.setHorizontalAlignment( SwingConstants.CENTER );
|
||||
avatarButton.setMaximumSize( new Dimension( Integer.MAX_VALUE, 0 ) );
|
||||
|
||||
// -
|
||||
add( Components.strut( Components.margin() ) );
|
||||
|
||||
// User Selection
|
||||
add( Components.comboBox( usersModel, user -> ifNotNull( user, MPUser::getFullName ) ) );
|
||||
|
||||
MPFileUserManager.get().addListener( this );
|
||||
MasterPassword.get().addListener( this );
|
||||
}
|
||||
|
||||
private void setAvatar() {
|
||||
MPUser<?> selectedUser = usersModel.getSelectedItem();
|
||||
if (selectedUser == null)
|
||||
return;
|
||||
|
||||
selectedUser.setAvatar( (selectedUser.getAvatar() + 1) % Res.icons().avatars() );
|
||||
avatarButton.setIcon( Res.icons().avatar( selectedUser.getAvatar() ) );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFilesUpdated(final ImmutableSortedSet<MPFileUser> files) {
|
||||
usersModel.set( files );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserSelected(@Nullable final MPUser<?> user) {
|
||||
usersModel.setSelectedItem( user );
|
||||
avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) );
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
//==============================================================================
|
||||
// This file is part of Master Password.
|
||||
// Copyright (c) 2011-2017, Maarten Billemont.
|
||||
//
|
||||
// Master Password is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Master Password is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You can find a copy of the GNU General Public License in the
|
||||
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
|
||||
//==============================================================================
|
||||
|
||||
package com.lyndir.masterpassword.gui.view;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
import com.lyndir.masterpassword.MPAlgorithm;
|
||||
import com.lyndir.masterpassword.MPResultType;
|
||||
import com.lyndir.masterpassword.gui.Res;
|
||||
import com.lyndir.masterpassword.gui.model.MPIncognitoSite;
|
||||
import com.lyndir.masterpassword.gui.model.MPIncognitoUser;
|
||||
import com.lyndir.masterpassword.gui.util.Components;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2014-06-11
|
||||
*/
|
||||
@SuppressWarnings({ "serial", "MagicNumber" })
|
||||
public class IncognitoAuthenticationPanel extends AuthenticationPanel<MPIncognitoUser> implements DocumentListener, ActionListener {
|
||||
|
||||
private final JTextField fullNameField;
|
||||
private final JPasswordField masterPasswordField;
|
||||
|
||||
public IncognitoAuthenticationPanel(final UnlockFrame unlockFrame) {
|
||||
|
||||
// Full Name
|
||||
super( unlockFrame );
|
||||
add( Components.stud() );
|
||||
|
||||
JLabel fullNameLabel = Components.label( "Full Name:" );
|
||||
add( fullNameLabel );
|
||||
|
||||
fullNameField = Components.textField();
|
||||
fullNameField.setFont( Res.valueFont().deriveFont( 12f ) );
|
||||
fullNameField.getDocument().addDocumentListener( this );
|
||||
fullNameField.addActionListener( this );
|
||||
add( fullNameField );
|
||||
add( Components.stud() );
|
||||
|
||||
// Master Password
|
||||
JLabel masterPasswordLabel = Components.label( "Master Password:" );
|
||||
add( masterPasswordLabel );
|
||||
|
||||
masterPasswordField = Components.passwordField();
|
||||
masterPasswordField.addActionListener( this );
|
||||
masterPasswordField.getDocument().addDocumentListener( this );
|
||||
add( masterPasswordField );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getFocusComponent() {
|
||||
return fullNameField;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
masterPasswordField.setText( "" );
|
||||
}
|
||||
|
||||
@Override
|
||||
public PasswordFrame<MPIncognitoUser, ?> newPasswordFrame() {
|
||||
return new PasswordFrame<MPIncognitoUser, MPIncognitoSite>( Preconditions.checkNotNull( getSelectedUser() ) ) {
|
||||
@Override
|
||||
protected MPIncognitoSite createSite(final MPIncognitoUser user, final String siteName, final UnsignedInteger siteCounter,
|
||||
final MPResultType resultType, final MPAlgorithm algorithm) {
|
||||
return new MPIncognitoSite( user, siteName, algorithm, siteCounter, resultType, null );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected MPIncognitoUser getSelectedUser() {
|
||||
return new MPIncognitoUser( fullNameField.getText() );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public char[] getMasterPassword() {
|
||||
return masterPasswordField.getPassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertUpdate(final DocumentEvent e) {
|
||||
updateUser( false );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(final DocumentEvent e) {
|
||||
updateUser( false );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(final DocumentEvent e) {
|
||||
updateUser( false );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(final ActionEvent e) {
|
||||
updateUser( false );
|
||||
unlockFrame.trySignIn( fullNameField, masterPasswordField );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.lyndir.masterpassword.gui.view;
|
||||
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import com.lyndir.masterpassword.gui.util.Components;
|
||||
import com.lyndir.masterpassword.gui.util.Res;
|
||||
import com.lyndir.masterpassword.model.impl.MPFileUserManager;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ComponentAdapter;
|
||||
import java.awt.event.ComponentEvent;
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.BevelBorder;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-14
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class MasterPasswordFrame extends JFrame {
|
||||
|
||||
private static final Logger logger = Logger.get( MasterPasswordFrame.class );
|
||||
|
||||
private final UserContentPanel userContent;
|
||||
|
||||
@SuppressWarnings("MagicNumber")
|
||||
public MasterPasswordFrame() {
|
||||
super( "Master Password" );
|
||||
|
||||
JPanel root, userPanel;
|
||||
setContentPane( root = Components.borderPanel( Res.colors().frameBg(), BoxLayout.PAGE_AXIS ) );
|
||||
|
||||
root.add( new FilesPanel() );
|
||||
root.add( Components.strut() );
|
||||
root.add( userPanel = Components.panel( new BorderLayout( 0, 0 ) ) );
|
||||
|
||||
userPanel.add( Components.borderPanel(
|
||||
BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ),
|
||||
Res.colors().controlBg(), BoxLayout.PAGE_AXIS, userContent = new UserContentPanel() ), BorderLayout.CENTER );
|
||||
userPanel.add( userContent.getUserToolbar(), BorderLayout.LINE_START );
|
||||
userPanel.add( userContent.getSiteToolbar(), BorderLayout.LINE_END );
|
||||
|
||||
addComponentListener( new ComponentHandler() );
|
||||
setPreferredSize( new Dimension( 800, 560 ) );
|
||||
setDefaultCloseOperation( DISPOSE_ON_CLOSE );
|
||||
pack();
|
||||
|
||||
setLocationRelativeTo( null );
|
||||
setLocationByPlatform( true );
|
||||
}
|
||||
|
||||
private class ComponentHandler extends ComponentAdapter {
|
||||
|
||||
@Override
|
||||
public void componentShown(final ComponentEvent e) {
|
||||
MPFileUserManager.get().reload();
|
||||
userContent.transferFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
//==============================================================================
|
||||
// This file is part of Master Password.
|
||||
// Copyright (c) 2011-2017, Maarten Billemont.
|
||||
//
|
||||
// Master Password is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Master Password is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You can find a copy of the GNU General Public License in the
|
||||
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
|
||||
//==============================================================================
|
||||
|
||||
package com.lyndir.masterpassword.gui.view;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import com.lyndir.masterpassword.MPAlgorithm;
|
||||
import com.lyndir.masterpassword.MPResultType;
|
||||
import com.lyndir.masterpassword.gui.Res;
|
||||
import com.lyndir.masterpassword.gui.util.Components;
|
||||
import com.lyndir.masterpassword.model.MPUser;
|
||||
import com.lyndir.masterpassword.model.impl.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import javax.swing.plaf.metal.MetalComboBoxEditor;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2014-06-11
|
||||
*/
|
||||
public class ModelAuthenticationPanel extends AuthenticationPanel<MPFileUser> implements ItemListener, ActionListener, DocumentListener {
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
private static final Logger logger = Logger.get( ModelAuthenticationPanel.class );
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final JComboBox<MPFileUser> userField;
|
||||
private final JLabel masterPasswordLabel;
|
||||
private final JPasswordField masterPasswordField;
|
||||
|
||||
public ModelAuthenticationPanel(final UnlockFrame unlockFrame) {
|
||||
super( unlockFrame );
|
||||
add( Components.stud() );
|
||||
|
||||
// Avatar
|
||||
avatarLabel.addMouseListener( new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(final MouseEvent e) {
|
||||
MPFileUser selectedUser = getSelectedUser();
|
||||
if (selectedUser != null) {
|
||||
selectedUser.setAvatar( selectedUser.getAvatar() + 1 );
|
||||
updateUser( false );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
// User
|
||||
JLabel userLabel = Components.label( "User:" );
|
||||
add( userLabel );
|
||||
|
||||
userField = Components.comboBox( readConfigUsers() );
|
||||
userField.setFont( Res.valueFont().deriveFont( userField.getFont().getSize2D() ) );
|
||||
userField.addItemListener( this );
|
||||
userField.addActionListener( this );
|
||||
userField.setRenderer( new DefaultListCellRenderer() {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
|
||||
final boolean isSelected, final boolean cellHasFocus) {
|
||||
String userValue = ((MPUser<MPFileSite>) value).getFullName();
|
||||
return super.getListCellRendererComponent( list, userValue, index, isSelected, cellHasFocus );
|
||||
}
|
||||
} );
|
||||
userField.setEditor( new MetalComboBoxEditor() {
|
||||
@Override
|
||||
protected JTextField createEditorComponent() {
|
||||
JTextField editorComponents = Components.textField();
|
||||
editorComponents.setForeground( Color.red );
|
||||
return editorComponents;
|
||||
}
|
||||
} );
|
||||
|
||||
add( userField );
|
||||
add( Components.stud() );
|
||||
|
||||
// Master Password
|
||||
masterPasswordLabel = Components.label( "Master Password:" );
|
||||
add( masterPasswordLabel );
|
||||
|
||||
masterPasswordField = Components.passwordField();
|
||||
masterPasswordField.addActionListener( this );
|
||||
masterPasswordField.getDocument().addDocumentListener( this );
|
||||
add( masterPasswordField );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getFocusComponent() {
|
||||
return masterPasswordField.isVisible()? masterPasswordField: null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateUser(boolean repack) {
|
||||
MPFileUser selectedUser = getSelectedUser();
|
||||
if (selectedUser != null) {
|
||||
avatarLabel.setIcon( Res.avatar( selectedUser.getAvatar() ) );
|
||||
boolean showPasswordField = !selectedUser.isMasterKeyAvailable(); // TODO: is this the same as keySaved()?
|
||||
if (masterPasswordField.isVisible() != showPasswordField) {
|
||||
masterPasswordLabel.setVisible( showPasswordField );
|
||||
masterPasswordField.setVisible( showPasswordField );
|
||||
repack = true;
|
||||
}
|
||||
}
|
||||
|
||||
super.updateUser( repack );
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected MPFileUser getSelectedUser() {
|
||||
int selectedIndex = userField.getSelectedIndex();
|
||||
if (selectedIndex < 0)
|
||||
return null;
|
||||
|
||||
return userField.getModel().getElementAt( selectedIndex );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public char[] getMasterPassword() {
|
||||
return masterPasswordField.getPassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<? extends JButton> getButtons() {
|
||||
return ImmutableList.of( new JButton( Res.iconAdd() ) {
|
||||
{
|
||||
addActionListener( new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(final ActionEvent e) {
|
||||
String fullName = JOptionPane.showInputDialog( ModelAuthenticationPanel.this, //
|
||||
"Enter your full name, ensuring it is correctly spelled and capitalized:",
|
||||
"New User", JOptionPane.QUESTION_MESSAGE );
|
||||
MPFileUserManager.get().addUser( new MPFileUser( fullName ) );
|
||||
userField.setModel( new DefaultComboBoxModel<>( readConfigUsers() ) );
|
||||
updateUser( true );
|
||||
}
|
||||
} );
|
||||
setToolTipText( "Add a new user to the list." );
|
||||
}
|
||||
}, new JButton( Res.iconDelete() ) {
|
||||
{
|
||||
addActionListener( new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(final ActionEvent e) {
|
||||
MPFileUser deleteUser = getSelectedUser();
|
||||
if (deleteUser == null)
|
||||
return;
|
||||
|
||||
if (JOptionPane.showConfirmDialog( ModelAuthenticationPanel.this, //
|
||||
strf( "Are you sure you want to delete the user and sites remembered for:%n%s.",
|
||||
deleteUser.getFullName() ), //
|
||||
"Delete User", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE )
|
||||
== JOptionPane.CANCEL_OPTION)
|
||||
return;
|
||||
|
||||
MPFileUserManager.get().deleteUser( deleteUser );
|
||||
userField.setModel( new DefaultComboBoxModel<>( readConfigUsers() ) );
|
||||
updateUser( true );
|
||||
}
|
||||
} );
|
||||
setToolTipText( "Delete the selected user." );
|
||||
}
|
||||
}, new JButton( Res.iconQuestion() ) {
|
||||
{
|
||||
addActionListener( e -> JOptionPane.showMessageDialog(
|
||||
ModelAuthenticationPanel.this, //
|
||||
strf( "Reads users and sites from the directory at:%n%s",
|
||||
MPFileUserManager.get().getPath().getAbsolutePath() ), //
|
||||
"Help", JOptionPane.INFORMATION_MESSAGE ) );
|
||||
setToolTipText( "More information." );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
masterPasswordField.setText( "" );
|
||||
}
|
||||
|
||||
@Override
|
||||
public PasswordFrame<MPFileUser, MPFileSite> newPasswordFrame() {
|
||||
return new PasswordFrame<MPFileUser, MPFileSite>( Preconditions.checkNotNull( getSelectedUser() ) ) {
|
||||
@Override
|
||||
protected MPFileSite createSite(final MPFileUser user, final String siteName, final UnsignedInteger siteCounter,
|
||||
final MPResultType resultType,
|
||||
final MPAlgorithm algorithm) {
|
||||
return new MPFileSite( user, siteName, algorithm, siteCounter, resultType );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static MPFileUser[] readConfigUsers() {
|
||||
return MPFileUserManager.get().getUsers().toArray( new MPFileUser[0] );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void itemStateChanged(final ItemEvent e) {
|
||||
updateUser( false );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(final ActionEvent e) {
|
||||
updateUser( false );
|
||||
unlockFrame.trySignIn( userField );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertUpdate(final DocumentEvent e) {
|
||||
updateUser( false );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(final DocumentEvent e) {
|
||||
updateUser( false );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(final DocumentEvent e) {
|
||||
updateUser( false );
|
||||
}
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
//==============================================================================
|
||||
// This file is part of Master Password.
|
||||
// Copyright (c) 2011-2017, Maarten Billemont.
|
||||
//
|
||||
// Master Password is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Master Password is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You can find a copy of the GNU General Public License in the
|
||||
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
|
||||
//==============================================================================
|
||||
|
||||
package com.lyndir.masterpassword.gui.view;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
import com.google.common.util.concurrent.*;
|
||||
import com.lyndir.masterpassword.*;
|
||||
import com.lyndir.masterpassword.gui.Res;
|
||||
import com.lyndir.masterpassword.gui.util.Components;
|
||||
import com.lyndir.masterpassword.gui.util.UnsignedIntegerModel;
|
||||
import com.lyndir.masterpassword.model.MPSite;
|
||||
import com.lyndir.masterpassword.model.MPUser;
|
||||
import com.lyndir.masterpassword.model.impl.MPFileSite;
|
||||
import com.lyndir.masterpassword.model.impl.MPFileUser;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.awt.datatransfer.Transferable;
|
||||
import java.awt.event.*;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2014-06-08
|
||||
*/
|
||||
public abstract class PasswordFrame<U extends MPUser<S>, S extends MPSite<?>> extends JFrame implements DocumentListener {
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final Components.GradientPanel root;
|
||||
private final JTextField siteNameField;
|
||||
private final JButton siteActionButton;
|
||||
private final JComboBox<MPAlgorithm.Version> siteVersionField;
|
||||
private final JSpinner siteCounterField;
|
||||
private final UnsignedIntegerModel siteCounterModel;
|
||||
private final JComboBox<MPResultType> resultTypeField;
|
||||
private final JPasswordField passwordField;
|
||||
private final JLabel tipLabel;
|
||||
private final JCheckBox maskPasswordField;
|
||||
private final char passwordEchoChar;
|
||||
private final Font passwordEchoFont;
|
||||
private final U user;
|
||||
|
||||
@Nullable
|
||||
private S currentSite;
|
||||
private boolean updatingUI;
|
||||
|
||||
@SuppressWarnings("MagicNumber")
|
||||
protected PasswordFrame(final U user) {
|
||||
super( "Master Password" );
|
||||
this.user = user;
|
||||
|
||||
setDefaultCloseOperation( DISPOSE_ON_CLOSE );
|
||||
setContentPane( root = Components.gradientPanel( new FlowLayout(), Res.colors().frameBg() ) );
|
||||
root.setLayout( new BoxLayout( root, BoxLayout.PAGE_AXIS ) );
|
||||
root.setBorder( BorderFactory.createEmptyBorder( 20, 20, 20, 20 ) );
|
||||
|
||||
// Site
|
||||
JPanel sitePanel = Components.boxLayout( BoxLayout.PAGE_AXIS );
|
||||
sitePanel.setOpaque( true );
|
||||
sitePanel.setBackground( Res.colors().controlBg() );
|
||||
sitePanel.setBorder( BorderFactory.createEmptyBorder( 20, 20, 20, 20 ) );
|
||||
root.add( Components.borderPanel( sitePanel, BorderFactory.createRaisedBevelBorder(), Res.colors().frameBg() ) );
|
||||
|
||||
// User
|
||||
sitePanel.add( Components.label( strf( "Generating passwords for: %s", user.getFullName() ), SwingConstants.CENTER ) );
|
||||
sitePanel.add( Components.stud() );
|
||||
|
||||
// Site Name
|
||||
sitePanel.add( Components.label( "Site Name:" ) );
|
||||
JComponent siteControls = Components.boxLayout( BoxLayout.LINE_AXIS, //
|
||||
siteNameField = Components.textField(), Components.stud(),
|
||||
siteActionButton = Components.button( "Add Site" ) );
|
||||
siteNameField.getDocument().addDocumentListener( this );
|
||||
siteNameField.addActionListener( new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(final ActionEvent e) {
|
||||
Futures.addCallback( updatePassword( true ), new FutureCallback<String>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable final String sitePassword) {
|
||||
Transferable clipboardContents = new StringSelection( sitePassword );
|
||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null );
|
||||
|
||||
SwingUtilities.invokeLater( () -> {
|
||||
passwordField.setText( null );
|
||||
siteNameField.setText( null );
|
||||
|
||||
dispatchEvent( new WindowEvent( PasswordFrame.this, WindowEvent.WINDOW_CLOSING ) );
|
||||
} );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@Nonnull final Throwable t) {
|
||||
}
|
||||
} );
|
||||
}
|
||||
} );
|
||||
siteActionButton.addActionListener( e -> {
|
||||
if (currentSite == null)
|
||||
return;
|
||||
if (currentSite instanceof MPFileSite)
|
||||
this.user.deleteSite( currentSite );
|
||||
else
|
||||
this.user.addSite( currentSite );
|
||||
siteNameField.requestFocus();
|
||||
|
||||
updatePassword( true );
|
||||
} );
|
||||
sitePanel.add( siteControls );
|
||||
sitePanel.add( Components.stud() );
|
||||
|
||||
// Site Type & Counter
|
||||
siteCounterModel = new UnsignedIntegerModel( UnsignedInteger.ONE, UnsignedInteger.ONE );
|
||||
MPResultType[] types = Iterables.toArray( MPResultType.forClass( MPResultTypeClass.Template ), MPResultType.class );
|
||||
JComponent siteSettings = Components.boxLayout( BoxLayout.LINE_AXIS, //
|
||||
resultTypeField = Components.comboBox( types ), //
|
||||
Components.stud(), //
|
||||
siteVersionField = Components.comboBox( MPAlgorithm.Version.values() ), //
|
||||
Components.stud(), //
|
||||
siteCounterField = Components.spinner( siteCounterModel ) );
|
||||
sitePanel.add( siteSettings );
|
||||
resultTypeField.setFont( Res.valueFont().deriveFont( resultTypeField.getFont().getSize2D() ) );
|
||||
resultTypeField.setSelectedItem( user.getAlgorithm().mpw_default_result_type() );
|
||||
resultTypeField.addItemListener( e -> updatePassword( true ) );
|
||||
|
||||
siteVersionField.setFont( Res.valueFont().deriveFont( siteVersionField.getFont().getSize2D() ) );
|
||||
siteVersionField.setAlignmentX( RIGHT_ALIGNMENT );
|
||||
siteVersionField.setSelectedItem( user.getAlgorithm() );
|
||||
siteVersionField.addItemListener( e -> updatePassword( true ) );
|
||||
|
||||
siteCounterField.setFont( Res.valueFont().deriveFont( siteCounterField.getFont().getSize2D() ) );
|
||||
siteCounterField.setAlignmentX( RIGHT_ALIGNMENT );
|
||||
siteCounterField.addChangeListener( e -> updatePassword( true ) );
|
||||
|
||||
// Mask
|
||||
maskPasswordField = Components.checkBox( "Hide Password" );
|
||||
maskPasswordField.setAlignmentX( Component.CENTER_ALIGNMENT );
|
||||
maskPasswordField.setSelected( true );
|
||||
maskPasswordField.addItemListener( e -> updateMask() );
|
||||
|
||||
// Password
|
||||
passwordField = Components.passwordField();
|
||||
passwordField.setAlignmentX( Component.CENTER_ALIGNMENT );
|
||||
passwordField.setHorizontalAlignment( SwingConstants.CENTER );
|
||||
passwordField.putClientProperty( "JPasswordField.cutCopyAllowed", true );
|
||||
passwordField.setEditable( false );
|
||||
passwordField.setBackground( null );
|
||||
passwordField.setBorder( null );
|
||||
passwordEchoChar = passwordField.getEchoChar();
|
||||
passwordEchoFont = passwordField.getFont().deriveFont( 40f );
|
||||
updateMask();
|
||||
|
||||
// Tip
|
||||
tipLabel = Components.label( " ", SwingConstants.CENTER );
|
||||
tipLabel.setAlignmentX( Component.CENTER_ALIGNMENT );
|
||||
JPanel passwordContainer = Components.boxLayout( BoxLayout.PAGE_AXIS, maskPasswordField, Box.createGlue(), passwordField,
|
||||
Box.createGlue(), tipLabel );
|
||||
passwordContainer.setOpaque( true );
|
||||
passwordContainer.setBackground( Color.white );
|
||||
passwordContainer.setBorder( BorderFactory.createEmptyBorder( 8, 8, 8, 8 ) );
|
||||
root.add( Box.createVerticalStrut( 8 ) );
|
||||
root.add( Components.borderPanel( passwordContainer, BorderFactory.createLoweredSoftBevelBorder(), Res.colors().frameBg() ),
|
||||
BorderLayout.SOUTH );
|
||||
|
||||
pack();
|
||||
setMinimumSize( new Dimension( Math.max( 600, getPreferredSize().width ), Math.max( 300, getPreferredSize().height ) ) );
|
||||
pack();
|
||||
|
||||
setLocationByPlatform( true );
|
||||
setLocationRelativeTo( null );
|
||||
}
|
||||
|
||||
@SuppressWarnings("MagicNumber")
|
||||
private void updateMask() {
|
||||
passwordField.setEchoChar( maskPasswordField.isSelected()? passwordEchoChar: (char) 0 );
|
||||
passwordField.setFont( maskPasswordField.isSelected()? passwordEchoFont: Res.bigValueFont().deriveFont( 40f ) );
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private ListenableFuture<String> updatePassword(final boolean allowNameCompletion) {
|
||||
|
||||
String siteNameQuery = siteNameField.getText();
|
||||
if (updatingUI)
|
||||
return Futures.immediateCancelledFuture();
|
||||
if ((siteNameQuery == null) || siteNameQuery.isEmpty() || !user.isMasterKeyAvailable()) {
|
||||
siteActionButton.setVisible( false );
|
||||
tipLabel.setText( null );
|
||||
passwordField.setText( null );
|
||||
return Futures.immediateCancelledFuture();
|
||||
}
|
||||
|
||||
MPResultType resultType = resultTypeField.getModel().getElementAt( resultTypeField.getSelectedIndex() );
|
||||
MPAlgorithm siteAlgorithm = siteVersionField.getItemAt( siteVersionField.getSelectedIndex() ).getAlgorithm();
|
||||
UnsignedInteger siteCounter = siteCounterModel.getNumber();
|
||||
|
||||
Collection<S> siteResults = user.findSites( siteNameQuery );
|
||||
if (!allowNameCompletion)
|
||||
siteResults = siteResults.stream().filter(
|
||||
siteResult -> (siteResult != null) && siteNameQuery.equals( siteResult.getName() ) ).collect( Collectors.toList() );
|
||||
S site = ifNotNullElse( Iterables.getFirst( siteResults, null ),
|
||||
createSite( user, siteNameQuery, siteCounter, resultType, siteAlgorithm ) );
|
||||
if ((currentSite != null) && currentSite.getName().equals( site.getName() )) {
|
||||
site.setResultType( resultType );
|
||||
site.setAlgorithm( siteAlgorithm );
|
||||
site.setCounter( siteCounter );
|
||||
}
|
||||
|
||||
ListenableFuture<String> passwordFuture = Res.execute( this, () -> site.getResult( MPKeyPurpose.Authentication, null, null ) );
|
||||
Futures.addCallback( passwordFuture, new FutureCallback<String>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable final String sitePassword) {
|
||||
SwingUtilities.invokeLater( () -> {
|
||||
updatingUI = true;
|
||||
currentSite = site;
|
||||
siteActionButton.setVisible( user instanceof MPFileUser );
|
||||
if (currentSite instanceof MPFileSite)
|
||||
siteActionButton.setText( "Delete Site" );
|
||||
else
|
||||
siteActionButton.setText( "Add Site" );
|
||||
resultTypeField.setSelectedItem( currentSite.getResultType() );
|
||||
siteVersionField.setSelectedItem( currentSite.getAlgorithm() );
|
||||
siteCounterField.setValue( currentSite.getCounter() );
|
||||
siteNameField.setText( currentSite.getName() );
|
||||
if (siteNameField.getText().startsWith( siteNameQuery ))
|
||||
siteNameField.select( siteNameQuery.length(), siteNameField.getText().length() );
|
||||
|
||||
passwordField.setText( sitePassword );
|
||||
tipLabel.setText( "Press [Enter] to copy the password. Then paste it into the password field." );
|
||||
updatingUI = false;
|
||||
} );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@Nonnull final Throwable t) {
|
||||
}
|
||||
} );
|
||||
|
||||
return passwordFuture;
|
||||
}
|
||||
|
||||
protected abstract S createSite(U user, String siteName, UnsignedInteger siteCounter, MPResultType resultType, MPAlgorithm algorithm);
|
||||
|
||||
@Override
|
||||
public void insertUpdate(final DocumentEvent e) {
|
||||
updatePassword( true );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(final DocumentEvent e) {
|
||||
updatePassword( false );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(final DocumentEvent e) {
|
||||
updatePassword( true );
|
||||
}
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
//==============================================================================
|
||||
// This file is part of Master Password.
|
||||
// Copyright (c) 2011-2017, Maarten Billemont.
|
||||
//
|
||||
// Master Password is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Master Password is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You can find a copy of the GNU General Public License in the
|
||||
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
|
||||
//==============================================================================
|
||||
|
||||
package com.lyndir.masterpassword.gui.view;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||
|
||||
import com.lyndir.masterpassword.MPAlgorithmException;
|
||||
import com.lyndir.masterpassword.MPIdenticon;
|
||||
import com.lyndir.masterpassword.gui.Res;
|
||||
import com.lyndir.masterpassword.gui.util.Components;
|
||||
import com.lyndir.masterpassword.model.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.*;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2014-06-08
|
||||
*/
|
||||
@SuppressWarnings({ "MagicNumber", "serial" })
|
||||
public class UnlockFrame extends JFrame {
|
||||
|
||||
private final SignInCallback signInCallback;
|
||||
private final Components.GradientPanel root;
|
||||
private final JLabel identiconLabel;
|
||||
private final JButton signInButton;
|
||||
private final JPanel authenticationContainer;
|
||||
private AuthenticationPanel<?> authenticationPanel;
|
||||
private Future<?> identiconFuture;
|
||||
private boolean incognito;
|
||||
@Nullable
|
||||
private MPUser<? extends MPSite<?>> user;
|
||||
|
||||
public UnlockFrame(final SignInCallback signInCallback) {
|
||||
super( "Unlock Master Password" );
|
||||
this.signInCallback = signInCallback;
|
||||
|
||||
setDefaultCloseOperation( DISPOSE_ON_CLOSE );
|
||||
addWindowFocusListener( new WindowAdapter() {
|
||||
@Override
|
||||
public void windowGainedFocus(final WindowEvent e) {
|
||||
root.setGradientColor( Res.colors().frameBg() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void windowLostFocus(final WindowEvent e) {
|
||||
root.setGradientColor( Color.RED );
|
||||
}
|
||||
} );
|
||||
|
||||
// Sign In
|
||||
JPanel signInBox = Components.boxLayout( BoxLayout.LINE_AXIS, Box.createGlue(), signInButton = Components.button( "Sign In" ),
|
||||
Box.createGlue() );
|
||||
signInBox.setBackground( null );
|
||||
|
||||
setContentPane( root = Components.gradientPanel( new FlowLayout(), Res.colors().frameBg() ) );
|
||||
root.setLayout( new BoxLayout( root, BoxLayout.PAGE_AXIS ) );
|
||||
root.setBorder( BorderFactory.createEmptyBorder( 20, 20, 20, 20 ) );
|
||||
root.add( Components.borderPanel( authenticationContainer = Components.boxLayout( BoxLayout.PAGE_AXIS ),
|
||||
BorderFactory.createRaisedBevelBorder(), Res.colors().frameBg() ) );
|
||||
root.add( Box.createVerticalStrut( 8 ) );
|
||||
root.add( identiconLabel = Components.label( " ", SwingConstants.CENTER ) );
|
||||
root.add( Box.createVerticalStrut( 8 ) );
|
||||
root.add( signInBox );
|
||||
|
||||
authenticationContainer.setOpaque( true );
|
||||
authenticationContainer.setBackground( Res.colors().controlBg() );
|
||||
authenticationContainer.setBorder( BorderFactory.createEmptyBorder( 20, 20, 20, 20 ) );
|
||||
identiconLabel.setFont( Res.emoticonsFont().deriveFont( 14.f ) );
|
||||
identiconLabel.setToolTipText(
|
||||
strf( "A representation of your identity across all Master Password apps.%nIt should always be the same." ) );
|
||||
signInButton.addActionListener( new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(final ActionEvent e) {
|
||||
trySignIn();
|
||||
}
|
||||
} );
|
||||
|
||||
createAuthenticationPanel();
|
||||
|
||||
setLocationByPlatform( true );
|
||||
setLocationRelativeTo( null );
|
||||
}
|
||||
|
||||
protected void repack() {
|
||||
pack();
|
||||
setMinimumSize( new Dimension( Math.max( 300, getPreferredSize().width ), Math.max( 300, getPreferredSize().height ) ) );
|
||||
pack();
|
||||
}
|
||||
|
||||
private void createAuthenticationPanel() {
|
||||
authenticationContainer.removeAll();
|
||||
|
||||
if (incognito) {
|
||||
authenticationPanel = new IncognitoAuthenticationPanel( this );
|
||||
} else {
|
||||
authenticationPanel = new ModelAuthenticationPanel( this );
|
||||
}
|
||||
authenticationPanel.updateUser( false );
|
||||
authenticationContainer.add( authenticationPanel );
|
||||
authenticationContainer.add( Components.stud() );
|
||||
|
||||
JCheckBox incognitoCheckBox = Components.checkBox( "Incognito" );
|
||||
incognitoCheckBox.setToolTipText( "Log in without saving any information." );
|
||||
incognitoCheckBox.setSelected( incognito );
|
||||
incognitoCheckBox.addItemListener( e -> {
|
||||
incognito = incognitoCheckBox.isSelected();
|
||||
SwingUtilities.invokeLater( this::createAuthenticationPanel );
|
||||
} );
|
||||
|
||||
JComponent toolsPanel = Components.boxLayout( BoxLayout.LINE_AXIS, incognitoCheckBox, Box.createGlue() );
|
||||
authenticationContainer.add( toolsPanel );
|
||||
for (final JButton button : authenticationPanel.getButtons()) {
|
||||
toolsPanel.add( button );
|
||||
button.setBorder( BorderFactory.createEmptyBorder() );
|
||||
button.setMargin( new Insets( 0, 0, 0, 0 ) );
|
||||
button.setAlignmentX( RIGHT_ALIGNMENT );
|
||||
button.setContentAreaFilled( false );
|
||||
}
|
||||
|
||||
checkSignIn();
|
||||
validate();
|
||||
repack();
|
||||
|
||||
SwingUtilities.invokeLater( () -> ifNotNullElse( authenticationPanel.getFocusComponent(), signInButton ).requestFocusInWindow() );
|
||||
}
|
||||
|
||||
void updateUser(@Nullable final MPUser<? extends MPSite<?>> user) {
|
||||
this.user = user;
|
||||
checkSignIn();
|
||||
}
|
||||
|
||||
boolean checkSignIn() {
|
||||
if (identiconFuture != null)
|
||||
identiconFuture.cancel( false );
|
||||
identiconFuture = Res.schedule( this, () -> SwingUtilities.invokeLater( () -> {
|
||||
String fullName = (user == null)? "": user.getFullName();
|
||||
char[] masterPassword = authenticationPanel.getMasterPassword();
|
||||
|
||||
if (fullName.isEmpty() || (masterPassword.length == 0)) {
|
||||
identiconLabel.setText( " " );
|
||||
return;
|
||||
}
|
||||
|
||||
MPIdenticon identicon = new MPIdenticon( fullName, masterPassword );
|
||||
identiconLabel.setText( identicon.getText() );
|
||||
identiconLabel.setForeground(
|
||||
Res.colors().fromIdenticonColor( identicon.getColor(), Res.Colors.BackgroundMode.DARK ) );
|
||||
} ), 300, TimeUnit.MILLISECONDS );
|
||||
|
||||
String fullName = (user == null)? "": user.getFullName();
|
||||
char[] masterPassword = authenticationPanel.getMasterPassword();
|
||||
boolean enabled = !fullName.isEmpty() && (masterPassword.length > 0);
|
||||
signInButton.setEnabled( enabled );
|
||||
|
||||
return enabled;
|
||||
}
|
||||
|
||||
void trySignIn(final JComponent... signInComponents) {
|
||||
if ((user == null) || !checkSignIn())
|
||||
return;
|
||||
|
||||
for (final JComponent signInComponent : signInComponents)
|
||||
signInComponent.setEnabled( false );
|
||||
|
||||
signInButton.setEnabled( false );
|
||||
signInButton.setText( "Signing In..." );
|
||||
|
||||
Res.execute( this, () -> {
|
||||
try {
|
||||
user.authenticate( authenticationPanel.getMasterPassword() );
|
||||
|
||||
SwingUtilities.invokeLater( () -> {
|
||||
signInCallback.signedIn( authenticationPanel.newPasswordFrame() );
|
||||
dispose();
|
||||
} );
|
||||
}
|
||||
catch (final MPIncorrectMasterPasswordException | MPAlgorithmException e) {
|
||||
SwingUtilities.invokeLater( () -> {
|
||||
JOptionPane.showMessageDialog( null, e.getLocalizedMessage(), "Sign In Failed", JOptionPane.ERROR_MESSAGE );
|
||||
authenticationPanel.reset();
|
||||
signInButton.setText( "Sign In" );
|
||||
for (final JComponent signInComponent : signInComponents)
|
||||
signInComponent.setEnabled( true );
|
||||
checkSignIn();
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface SignInCallback {
|
||||
|
||||
void signedIn(PasswordFrame<?, ?> passwordFrame);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,987 @@
|
||||
package com.lyndir.masterpassword.gui.view;
|
||||
|
||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||
|
||||
import com.google.common.base.*;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||
import com.lyndir.lhunath.opal.system.util.ObjectUtils;
|
||||
import com.lyndir.masterpassword.*;
|
||||
import com.lyndir.masterpassword.gui.MPGuiConstants;
|
||||
import com.lyndir.masterpassword.gui.MasterPassword;
|
||||
import com.lyndir.masterpassword.gui.model.*;
|
||||
import com.lyndir.masterpassword.gui.util.*;
|
||||
import com.lyndir.masterpassword.gui.util.Platform;
|
||||
import com.lyndir.masterpassword.model.*;
|
||||
import com.lyndir.masterpassword.model.impl.*;
|
||||
import com.lyndir.masterpassword.util.Utilities;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.awt.datatransfer.Transferable;
|
||||
import java.awt.event.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.*;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import javax.swing.text.PlainDocument;
|
||||
|
||||
|
||||
/**
|
||||
* @author lhunath, 2018-07-14
|
||||
*/
|
||||
@SuppressWarnings("SerializableStoresNonSerializable")
|
||||
public class UserContentPanel extends JPanel implements MasterPassword.Listener, MPUser.Listener {
|
||||
|
||||
private static final Random random = new Random();
|
||||
private static final int SIZE_RESULT = 48;
|
||||
private static final Logger logger = Logger.get( UserContentPanel.class );
|
||||
private static final JButton iconButton = Components.button( Res.icons().user(), null, null );
|
||||
private static final KeyStroke copyLoginKeyStroke = KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK );
|
||||
|
||||
private final JButton addButton = Components.button( Res.icons().add(), event -> addUser(),
|
||||
"Add a new user to Master Password." );
|
||||
private final JButton importButton = Components.button( Res.icons().import_(), event -> importUser(),
|
||||
"Import a user from a backup file into Master Password." );
|
||||
private final JButton helpButton = Components.button( Res.icons().help(), event -> showHelp(),
|
||||
"Show information on how to use Master Password." );
|
||||
|
||||
private final JPanel userToolbar = Components.panel( BoxLayout.PAGE_AXIS );
|
||||
private final JPanel siteToolbar = Components.panel( BoxLayout.PAGE_AXIS );
|
||||
|
||||
@Nullable
|
||||
private MPUser<?> showingUser;
|
||||
private ContentMode contentMode;
|
||||
|
||||
public UserContentPanel() {
|
||||
userToolbar.setPreferredSize( iconButton.getPreferredSize() );
|
||||
siteToolbar.setPreferredSize( iconButton.getPreferredSize() );
|
||||
|
||||
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
|
||||
setBorder( Components.marginBorder() );
|
||||
showUser( null );
|
||||
|
||||
MasterPassword.get().addListener( this );
|
||||
}
|
||||
|
||||
protected JComponent getUserToolbar() {
|
||||
return userToolbar;
|
||||
}
|
||||
|
||||
protected JComponent getSiteToolbar() {
|
||||
return siteToolbar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserSelected(@Nullable final MPUser<?> user) {
|
||||
showUser( user );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserUpdated(final MPUser<?> user) {
|
||||
showUser( user );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserAuthenticated(final MPUser<?> user) {
|
||||
showUser( user );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserInvalidated(final MPUser<?> user) {
|
||||
showUser( user );
|
||||
}
|
||||
|
||||
private void showUser(@Nullable final MPUser<?> user) {
|
||||
Res.ui( () -> {
|
||||
if (showingUser != null)
|
||||
showingUser.removeListener( this );
|
||||
|
||||
ContentMode newContentMode = ContentMode.getContentMode( user );
|
||||
if ((newContentMode != contentMode) || !ObjectUtils.equals( showingUser, user )) {
|
||||
userToolbar.removeAll();
|
||||
siteToolbar.removeAll();
|
||||
removeAll();
|
||||
showingUser = user;
|
||||
switch (contentMode = newContentMode) {
|
||||
case NO_USER:
|
||||
add( new NoUserPanel() );
|
||||
break;
|
||||
case AUTHENTICATE:
|
||||
add( new AuthenticateUserPanel( Preconditions.checkNotNull( showingUser ) ) );
|
||||
break;
|
||||
case AUTHENTICATED:
|
||||
add( new AuthenticatedUserPanel( Preconditions.checkNotNull( showingUser ) ) );
|
||||
break;
|
||||
}
|
||||
revalidate();
|
||||
transferFocus();
|
||||
}
|
||||
|
||||
if (showingUser != null)
|
||||
showingUser.addListener( this );
|
||||
} );
|
||||
}
|
||||
|
||||
private void addUser() {
|
||||
JTextField nameField = Components.textField( "Robert Lee Mitchell", null );
|
||||
JCheckBox incognitoField = Components.checkBox( "<html>Incognito <em>(Do not save this user to disk)</em></html>" );
|
||||
if (JOptionPane.OK_OPTION != Components.showDialog( this, "Add User", new JOptionPane( Components.panel(
|
||||
BoxLayout.PAGE_AXIS,
|
||||
Components.label( "<html>Enter your full legal name:</html>" ),
|
||||
Components.strut(),
|
||||
nameField,
|
||||
Components.strut(),
|
||||
incognitoField ), JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION ) {
|
||||
@Override
|
||||
public void selectInitialValue() {
|
||||
nameField.requestFocusInWindow();
|
||||
}
|
||||
} ))
|
||||
return;
|
||||
String fullName = nameField.getText();
|
||||
if (Strings.isNullOrEmpty( fullName ))
|
||||
return;
|
||||
|
||||
if (incognitoField.isSelected())
|
||||
MasterPassword.get().activateUser( new MPIncognitoUser( fullName ) );
|
||||
else
|
||||
MasterPassword.get().activateUser( MPFileUserManager.get().add( fullName ) );
|
||||
}
|
||||
|
||||
private void importUser() {
|
||||
File importFile = Components.showLoadDialog( this, "Import User File" );
|
||||
if (importFile == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
MPFileUser importUser = MPFileUser.load( importFile );
|
||||
if (importUser == null) {
|
||||
JOptionPane.showMessageDialog(
|
||||
this, "Not a Master Password file.",
|
||||
"Import Failed", JOptionPane.ERROR_MESSAGE );
|
||||
return;
|
||||
}
|
||||
|
||||
JPasswordField passwordField = Components.passwordField();
|
||||
if (JOptionPane.OK_OPTION == Components.showDialog( this, "Import User", new JOptionPane( Components.panel(
|
||||
BoxLayout.PAGE_AXIS,
|
||||
Components.label( strf( "<html>Enter the master password to import <strong>%s</strong>:</html>",
|
||||
importUser.getFullName() ) ),
|
||||
Components.strut(),
|
||||
passwordField ), JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION ) {
|
||||
@Override
|
||||
public void selectInitialValue() {
|
||||
passwordField.requestFocusInWindow();
|
||||
}
|
||||
} )) {
|
||||
try {
|
||||
importUser.authenticate( passwordField.getPassword() );
|
||||
Optional<MPFileUser> existingUser = MPFileUserManager.get().getFiles().stream().filter(
|
||||
user -> user.getFullName().equalsIgnoreCase( importUser.getFullName() ) ).findFirst();
|
||||
if (existingUser.isPresent() && (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(
|
||||
this,
|
||||
strf( "<html>Importing user <strong>%s</strong> from this file will replace the existing user with the imported one.<br>"
|
||||
+ "Are you sure?<br><br>"
|
||||
+ "<em>Existing user last modified: %s<br>Imported user last modified: %s</em></html>",
|
||||
importUser.getFullName(),
|
||||
Res.format( existingUser.get().getLastUsed() ),
|
||||
Res.format( importUser.getLastUsed() ) ) )))
|
||||
return;
|
||||
|
||||
MasterPassword.get().activateUser( MPFileUserManager.get().add( importUser ) );
|
||||
}
|
||||
catch (final MPIncorrectMasterPasswordException | MPAlgorithmException e) {
|
||||
JOptionPane.showMessageDialog(
|
||||
this, e.getLocalizedMessage(),
|
||||
"Import Failed", JOptionPane.ERROR_MESSAGE );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (final IOException e) {
|
||||
logger.err( e, "While reading user import file." );
|
||||
JOptionPane.showMessageDialog(
|
||||
this, strf( "<html>Couldn't read import file:<br><pre>%s</pre></html>.", e.getLocalizedMessage() ),
|
||||
"Import Failed", JOptionPane.ERROR_MESSAGE );
|
||||
}
|
||||
catch (final MPMarshalException e) {
|
||||
logger.err( e, "While parsing user import file." );
|
||||
JOptionPane.showMessageDialog(
|
||||
this, strf( "<html>Couldn't parse import file:<br><pre>%s</pre></html>.", e.getLocalizedMessage() ),
|
||||
"Import Failed", JOptionPane.ERROR_MESSAGE );
|
||||
}
|
||||
}
|
||||
|
||||
private void showHelp() {
|
||||
JOptionPane.showMessageDialog( this, Components.linkLabel( strf(
|
||||
"<h1>Master Password - v%s</h1>"
|
||||
+ "<p>The primary goal of this application is to provide a reliable security solution that also "
|
||||
+ "makes you independent from your computer. If you lose access to this computer or your data, "
|
||||
+ "the application can regenerate all your secrets from scratch on any new device.</p>"
|
||||
+ "<h2>Opening Master Password</h2>"
|
||||
+ "<p>To use Master Password, simply open the application on your computer. "
|
||||
+ "Once running, you can bring up the user interface at any time by pressing the keys "
|
||||
+ "<strong><code>%s+%s</code></strong>."
|
||||
+ "<h2>Persistence</h2>"
|
||||
+ "<p>Though at the core, Master Password does not require the use of any form of data "
|
||||
+ "storage, the application does remember the names of the sites you've used in the past to "
|
||||
+ "make it easier for you to use them again in the future. All user information is saved in "
|
||||
+ "files on your computer at the following location:<br><pre>%s</pre></p>"
|
||||
+ "<p>You can read, modify, backup or place new files in this location as you see fit. "
|
||||
+ "Some people even configure this location to be synced between their different computers "
|
||||
+ "using services such as those provided by SpiderOak or Dropbox.</p>"
|
||||
+ "<hr><p><a href='https://masterpassword.app'>https://masterpassword.app</a> — by Maarten Billemont</p>",
|
||||
MasterPassword.get().version(),
|
||||
InputEvent.getModifiersExText( MPGuiConstants.ui_hotkey.getModifiers() ),
|
||||
KeyEvent.getKeyText( MPGuiConstants.ui_hotkey.getKeyCode() ),
|
||||
MPFileUserManager.get().getPath().getAbsolutePath() ) ),
|
||||
"About Master Password", JOptionPane.INFORMATION_MESSAGE );
|
||||
}
|
||||
|
||||
private enum ContentMode {
|
||||
NO_USER,
|
||||
AUTHENTICATE,
|
||||
AUTHENTICATED;
|
||||
|
||||
static ContentMode getContentMode(@Nullable final MPUser<?> user) {
|
||||
if (user == null)
|
||||
return NO_USER;
|
||||
else if (!user.isMasterKeyAvailable())
|
||||
return AUTHENTICATE;
|
||||
else
|
||||
return AUTHENTICATED;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final class NoUserPanel extends JPanel {
|
||||
|
||||
private NoUserPanel() {
|
||||
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
|
||||
|
||||
userToolbar.add( addButton );
|
||||
userToolbar.add( importButton );
|
||||
userToolbar.add( Box.createGlue() );
|
||||
userToolbar.add( helpButton );
|
||||
|
||||
add( Box.createGlue() );
|
||||
add( Components.heading( "Select a user to proceed." ) );
|
||||
add( Box.createGlue() );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final class AuthenticateUserPanel extends JPanel implements ActionListener, DocumentListener {
|
||||
|
||||
@Nonnull
|
||||
private final MPUser<?> user;
|
||||
|
||||
private final JButton exportButton = Components.button( Res.icons().export(), event -> exportUser(),
|
||||
"Export this user to a backup file." );
|
||||
private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteUser(),
|
||||
"Delete this user from Master Password." );
|
||||
private final JButton resetButton = Components.button( Res.icons().reset(), event -> resetUser(),
|
||||
"Change the master password for this user." );
|
||||
|
||||
private final JPasswordField masterPasswordField;
|
||||
private final JLabel errorLabel;
|
||||
private final JLabel identiconLabel;
|
||||
|
||||
private Future<?> identiconJob;
|
||||
|
||||
private AuthenticateUserPanel(@Nonnull final MPUser<?> user) {
|
||||
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
|
||||
|
||||
this.user = user;
|
||||
|
||||
userToolbar.add( addButton );
|
||||
userToolbar.add( importButton );
|
||||
userToolbar.add( exportButton );
|
||||
userToolbar.add( deleteButton );
|
||||
userToolbar.add( resetButton );
|
||||
userToolbar.add( Box.createGlue() );
|
||||
userToolbar.add( helpButton );
|
||||
|
||||
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
|
||||
add( Components.strut() );
|
||||
|
||||
add( identiconLabel = Components.label( SwingConstants.CENTER ) );
|
||||
identiconLabel.setFont( Res.fonts().emoticonsFont( Components.TEXT_SIZE_CONTROL ) );
|
||||
add( Box.createGlue() );
|
||||
|
||||
add( Components.label( "Master Password:" ) );
|
||||
add( Components.strut() );
|
||||
add( masterPasswordField = Components.passwordField() );
|
||||
masterPasswordField.addActionListener( this );
|
||||
masterPasswordField.getDocument().addDocumentListener( this );
|
||||
add( errorLabel = Components.label() );
|
||||
errorLabel.setForeground( Res.colors().errorFg() );
|
||||
add( Box.createGlue() );
|
||||
}
|
||||
|
||||
private void exportUser() {
|
||||
MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null;
|
||||
if (fileUser == null)
|
||||
return;
|
||||
|
||||
File exportFile = Components.showSaveDialog( this, "Export User File", fileUser.getFile().getName() );
|
||||
if (exportFile == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
Platform.get().show(
|
||||
Files.copy( fileUser.getFile().toPath(), exportFile.toPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES ).toFile() );
|
||||
}
|
||||
catch (final IOException e) {
|
||||
JOptionPane.showMessageDialog(
|
||||
this, e.getLocalizedMessage(),
|
||||
"Export Failed", JOptionPane.ERROR_MESSAGE );
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteUser() {
|
||||
MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null;
|
||||
if (fileUser == null)
|
||||
return;
|
||||
|
||||
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
|
||||
SwingUtilities.windowForComponent( this ), strf( "<html>Delete the user <strong>%s</strong>?<br><br><em>%s</em></html>",
|
||||
fileUser.getFullName(), fileUser.getFile().getName() ),
|
||||
"Delete User", JOptionPane.YES_NO_OPTION ))
|
||||
MPFileUserManager.get().delete( fileUser );
|
||||
}
|
||||
|
||||
private void resetUser() {
|
||||
JPasswordField passwordField = Components.passwordField();
|
||||
if (JOptionPane.OK_OPTION == Components.showDialog( this, "Reset User", new JOptionPane( Components.panel(
|
||||
BoxLayout.PAGE_AXIS,
|
||||
Components.label( strf( "<html>Enter the new master password for <strong>%s</strong>:</html>",
|
||||
user.getFullName() ) ),
|
||||
Components.strut(),
|
||||
passwordField,
|
||||
Components.strut(),
|
||||
Components.label( strf( "<html><em><strong>Note:</strong><br>Changing the master password "
|
||||
+ "will change all of the user's passwords.<br>"
|
||||
+ "Changing back to the original master password will also restore<br>"
|
||||
+ "the user's original passwords.</em></html>",
|
||||
user.getFullName() ) ) ), JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION ) {
|
||||
@Override
|
||||
public void selectInitialValue() {
|
||||
passwordField.requestFocusInWindow();
|
||||
}
|
||||
} )) {
|
||||
char[] masterPassword = passwordField.getPassword();
|
||||
if ((masterPassword != null) && (masterPassword.length > 0))
|
||||
try {
|
||||
user.reset();
|
||||
user.authenticate( masterPassword );
|
||||
}
|
||||
catch (final MPIncorrectMasterPasswordException e) {
|
||||
errorLabel.setText( e.getLocalizedMessage() );
|
||||
throw logger.bug( e );
|
||||
}
|
||||
catch (final MPAlgorithmException e) {
|
||||
logger.err( e, "While resetting master password." );
|
||||
errorLabel.setText( e.getLocalizedMessage() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(final ActionEvent event) {
|
||||
updateIdenticon();
|
||||
|
||||
char[] masterPassword = masterPasswordField.getPassword();
|
||||
Res.job( () -> {
|
||||
try {
|
||||
user.authenticate( masterPassword );
|
||||
}
|
||||
catch (final MPIncorrectMasterPasswordException e) {
|
||||
logger.wrn( e, "During user authentication for: %s", user );
|
||||
errorLabel.setText( e.getLocalizedMessage() );
|
||||
}
|
||||
catch (final MPAlgorithmException e) {
|
||||
logger.err( e, "During user authentication for: %s", user );
|
||||
errorLabel.setText( e.getLocalizedMessage() );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertUpdate(final DocumentEvent event) {
|
||||
update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(final DocumentEvent event) {
|
||||
update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(final DocumentEvent event) {
|
||||
update();
|
||||
}
|
||||
|
||||
private synchronized void update() {
|
||||
errorLabel.setText( " " );
|
||||
|
||||
if (identiconJob != null)
|
||||
identiconJob.cancel( true );
|
||||
|
||||
identiconJob = Res.job( this::updateIdenticon, 100 + random.nextInt( 100 ), TimeUnit.MILLISECONDS );
|
||||
}
|
||||
|
||||
private void updateIdenticon() {
|
||||
char[] masterPassword = masterPasswordField.getPassword();
|
||||
MPIdenticon identicon = ((masterPassword != null) && (masterPassword.length > 0))?
|
||||
new MPIdenticon( user.getFullName(), masterPassword ): null;
|
||||
|
||||
Res.ui( () -> {
|
||||
if (identicon != null) {
|
||||
identiconLabel.setForeground(
|
||||
Res.colors().fromIdenticonColor( identicon.getColor(), Res.Colors.BackgroundMode.LIGHT ) );
|
||||
identiconLabel.setText( identicon.getText() );
|
||||
} else {
|
||||
identiconLabel.setForeground( null );
|
||||
identiconLabel.setText( " " );
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final class AuthenticatedUserPanel extends JPanel implements KeyListener, MPUser.Listener {
|
||||
|
||||
private final JButton userButton = Components.button( Res.icons().user(), event -> showUserPreferences(),
|
||||
"Show user preferences." );
|
||||
private final JButton logoutButton = Components.button( Res.icons().lock(), event -> logoutUser(),
|
||||
"Sign out and lock user." );
|
||||
private final JButton settingsButton = Components.button( Res.icons().settings(), event -> showSiteSettings(),
|
||||
"Show site settings." );
|
||||
private final JButton questionsButton = Components.button( Res.icons().question(), event -> showSiteQuestions(),
|
||||
"Show site recovery questions." );
|
||||
private final JButton editButton = Components.button( Res.icons().edit(), event -> showSiteValues(),
|
||||
"Set/save personal password/login." );
|
||||
private final JButton keyButton = Components.button( Res.icons().key(), event -> showSiteKeys(),
|
||||
"Cryptographic site keys." );
|
||||
private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteSite(),
|
||||
"Delete the site from the user." );
|
||||
|
||||
@Nonnull
|
||||
private final MPUser<?> user;
|
||||
private final JLabel passwordLabel;
|
||||
private final JLabel passwordField;
|
||||
private final JLabel answerLabel;
|
||||
private final JLabel answerField;
|
||||
private final JLabel queryLabel;
|
||||
private final JTextField queryField;
|
||||
private final CollectionListModel<MPSite<?>> sitesModel;
|
||||
private final JList<MPSite<?>> sitesList;
|
||||
|
||||
private Future<?> updateSitesJob;
|
||||
|
||||
private AuthenticatedUserPanel(@Nonnull final MPUser<?> user) {
|
||||
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
|
||||
|
||||
this.user = user;
|
||||
|
||||
userToolbar.add( addButton );
|
||||
userToolbar.add( userButton );
|
||||
userToolbar.add( logoutButton );
|
||||
userToolbar.add( Box.createGlue() );
|
||||
userToolbar.add( helpButton );
|
||||
|
||||
siteToolbar.add( settingsButton );
|
||||
siteToolbar.add( questionsButton );
|
||||
siteToolbar.add( editButton );
|
||||
siteToolbar.add( keyButton );
|
||||
siteToolbar.add( deleteButton );
|
||||
settingsButton.setEnabled( false );
|
||||
questionsButton.setEnabled( false );
|
||||
editButton.setEnabled( false );
|
||||
keyButton.setEnabled( false );
|
||||
deleteButton.setEnabled( false );
|
||||
|
||||
answerLabel = Components.label( "Answer:" );
|
||||
answerField = Components.heading( SwingConstants.CENTER );
|
||||
answerField.setForeground( Res.colors().highlightFg() );
|
||||
answerField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
|
||||
|
||||
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
|
||||
|
||||
add( passwordLabel = Components.label( SwingConstants.CENTER ) );
|
||||
add( passwordField = Components.heading( SwingConstants.CENTER ) );
|
||||
passwordField.setForeground( Res.colors().highlightFg() );
|
||||
passwordField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
|
||||
add( Box.createGlue() );
|
||||
add( Components.strut() );
|
||||
|
||||
add( queryLabel = Components.label() );
|
||||
queryLabel.setText( strf( "%s's password for:", user.getFullName() ) );
|
||||
add( queryField = Components.textField( null, this::updateSites ) );
|
||||
queryField.putClientProperty( "JTextField.variant", "search" );
|
||||
queryField.addActionListener( this::useSite );
|
||||
queryField.getInputMap().put( copyLoginKeyStroke, JTextField.notifyAction );
|
||||
queryField.addKeyListener( this );
|
||||
queryField.requestFocusInWindow();
|
||||
add( Components.strut() );
|
||||
|
||||
add( Components.scrollPane( sitesList = Components.list(
|
||||
sitesModel = new CollectionListModel<MPSite<?>>().selection( this::showSiteResult ),
|
||||
this::getSiteDescription ) ) );
|
||||
add( Components.strut() );
|
||||
|
||||
add( Components.label( strf(
|
||||
"Press %s to copy password, %s+%s to copy login name.",
|
||||
KeyEvent.getKeyText( KeyEvent.VK_ENTER ),
|
||||
InputEvent.getModifiersExText( copyLoginKeyStroke.getModifiers() ),
|
||||
KeyEvent.getKeyText( copyLoginKeyStroke.getKeyCode() ) ) ) );
|
||||
|
||||
addHierarchyListener( e -> {
|
||||
if (null != SwingUtilities.windowForComponent( this ))
|
||||
user.addListener( this );
|
||||
else
|
||||
user.removeListener( this );
|
||||
} );
|
||||
}
|
||||
|
||||
public void showUserPreferences() {
|
||||
ImmutableList.Builder<Component> components = ImmutableList.builder();
|
||||
|
||||
MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null;
|
||||
if (fileUser != null)
|
||||
components.add( Components.label( "Default Password Type:" ),
|
||||
Components.comboBox( MPResultType.values(), MPResultType::getLongName,
|
||||
fileUser.getDefaultType(), fileUser::setDefaultType ),
|
||||
Components.strut() );
|
||||
|
||||
components.add( Components.label( "Default Algorithm:" ),
|
||||
Components.comboBox( MPAlgorithm.Version.values(), MPAlgorithm.Version::name,
|
||||
user.getAlgorithm().version(),
|
||||
version -> user.setAlgorithm( version.getAlgorithm() ) ) );
|
||||
|
||||
Components.showDialog( this, user.getFullName(), new JOptionPane( Components.panel(
|
||||
BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) );
|
||||
}
|
||||
|
||||
public void logoutUser() {
|
||||
user.invalidate();
|
||||
}
|
||||
|
||||
public void showSiteSettings() {
|
||||
MPSite<?> site = sitesModel.getSelectedItem();
|
||||
if (site == null)
|
||||
return;
|
||||
|
||||
ImmutableList.Builder<Component> components = ImmutableList.builder();
|
||||
components.add( Components.label( "Algorithm:" ),
|
||||
Components.comboBox( MPAlgorithm.Version.values(), MPAlgorithm.Version::name,
|
||||
site.getAlgorithm().version(),
|
||||
version -> site.setAlgorithm( version.getAlgorithm() ) ) );
|
||||
|
||||
components.add( Components.label( "Counter:" ),
|
||||
Components.spinner( new UnsignedIntegerModel( site.getCounter(), UnsignedInteger.ONE )
|
||||
.selection( site::setCounter ) ),
|
||||
Components.strut() );
|
||||
|
||||
components.add( Components.label( "Password Type:" ),
|
||||
Components.comboBox( MPResultType.values(), type -> getTypeDescription(
|
||||
type, user.getDefaultType(), user.getAlgorithm().mpw_default_result_type() ),
|
||||
site.getResultType(), site::setResultType ),
|
||||
Components.strut() );
|
||||
|
||||
components.add( Components.label( "Login Type:" ),
|
||||
Components.comboBox( MPResultType.values(), type -> getTypeDescription(
|
||||
type, user.getAlgorithm().mpw_default_login_type() ),
|
||||
site.getLoginType(), site::setLoginType ),
|
||||
Components.strut() );
|
||||
|
||||
MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null;
|
||||
if (fileSite != null)
|
||||
components.add( Components.label( "URL:" ),
|
||||
Components.textField( fileSite.getUrl(), fileSite::setUrl ),
|
||||
Components.strut() );
|
||||
|
||||
Components.showDialog( this, strf( "Settings for %s", site.getSiteName() ), new JOptionPane( Components.panel(
|
||||
BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) );
|
||||
}
|
||||
|
||||
private String getTypeDescription(final MPResultType type, final MPResultType... defaults) {
|
||||
boolean isDefault = false;
|
||||
for (final MPResultType d : defaults)
|
||||
if (isDefault = type == d)
|
||||
break;
|
||||
|
||||
return strf( "<html>%s%s%s, %s", isDefault? "<b>": "", type.getLongName(), isDefault? "</b>": "", type.getDescription() );
|
||||
}
|
||||
|
||||
public void showSiteQuestions() {
|
||||
MPSite<?> site = sitesModel.getSelectedItem();
|
||||
if (site == null)
|
||||
return;
|
||||
|
||||
CollectionListModel<MPQuestion> questionsModel = new CollectionListModel<MPQuestion>().selection( this::showQuestionResult );
|
||||
JList<MPQuestion> questionsList = Components.list(
|
||||
questionsModel, question -> Strings.isNullOrEmpty( question.getKeyword() )? "<site>": question.getKeyword() );
|
||||
JTextField queryField = Components.textField( null, query -> Res.job( () -> {
|
||||
Collection<MPQuestion> questions = new LinkedList<>( site.findQuestions( query ) );
|
||||
if (questions.stream().noneMatch( question -> question.getKeyword().equalsIgnoreCase( query ) ))
|
||||
questions.add( new MPNewQuestion( site, Utilities.ifNotNullElse( query, "" ) ) );
|
||||
|
||||
Res.ui( () -> questionsModel.set( questions ) );
|
||||
} ) );
|
||||
queryField.putClientProperty( "JTextField.variant", "search" );
|
||||
queryField.addActionListener( event -> useQuestion( questionsModel.getSelectedItem() ) );
|
||||
queryField.addKeyListener( new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(final KeyEvent event) {
|
||||
if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN))
|
||||
questionsList.dispatchEvent( event );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyReleased(final KeyEvent event) {
|
||||
if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN))
|
||||
questionsList.dispatchEvent( event );
|
||||
}
|
||||
} );
|
||||
|
||||
Components.showDialog( this, strf( "Recovery answers for %s", site.getSiteName() ), new JOptionPane( Components.panel(
|
||||
BoxLayout.PAGE_AXIS,
|
||||
Components.label( "Security Question Keyword:" ), queryField,
|
||||
Components.strut(),
|
||||
answerLabel, answerField,
|
||||
Components.strut(),
|
||||
Components.scrollPane( questionsList ) ) ) {
|
||||
@Override
|
||||
public void selectInitialValue() {
|
||||
queryField.requestFocusInWindow();
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
public void showSiteValues() {
|
||||
MPSite<?> site = sitesModel.getSelectedItem();
|
||||
if (site == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
JTextField passwordField = Components.textField( site.getResult(), null );
|
||||
JTextField loginField = Components.textField( site.getLogin(), null );
|
||||
passwordField.setEditable( site.getResultType().getTypeClass() == MPResultTypeClass.Stateful );
|
||||
loginField.setEditable( site.getLoginType().getTypeClass() == MPResultTypeClass.Stateful );
|
||||
|
||||
if (JOptionPane.OK_OPTION == Components.showDialog( this, site.getSiteName(), new JOptionPane(
|
||||
Components.panel(
|
||||
BoxLayout.PAGE_AXIS,
|
||||
Components.label( strf( "<html>Site Login (currently set to: <b>%s</b>):",
|
||||
getTypeDescription( site.getLoginType() ) ) ),
|
||||
loginField,
|
||||
Components.strut(),
|
||||
Components.label( strf( "<html>Site Password (currently set to: <b>%s</b>):",
|
||||
getTypeDescription( site.getResultType() ) ) ),
|
||||
passwordField,
|
||||
Components.strut(),
|
||||
Components.label( "<html>To save a personal value in these fields,\n" +
|
||||
"change the type to <b>Saved</b> in the site's settings." ) ),
|
||||
JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION ) {
|
||||
@Override
|
||||
public void selectInitialValue() {
|
||||
passwordField.requestFocusInWindow();
|
||||
}
|
||||
} )) {
|
||||
if (site instanceof MPFileSite) {
|
||||
MPFileSite fileSite = (MPFileSite) site;
|
||||
|
||||
if (site.getResultType().getTypeClass() == MPResultTypeClass.Stateful)
|
||||
fileSite.setSitePassword( site.getResultType(), passwordField.getText() );
|
||||
if (site.getLoginType().getTypeClass() == MPResultTypeClass.Stateful)
|
||||
fileSite.setLoginName( site.getLoginType(), loginField.getText() );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
|
||||
logger.err( e, "While computing site edit results." );
|
||||
}
|
||||
}
|
||||
|
||||
public void showSiteKeys() {
|
||||
MPSite<?> site = sitesModel.getSelectedItem();
|
||||
if (site == null)
|
||||
return;
|
||||
|
||||
JTextArea resultField = Components.textArea();
|
||||
resultField.setEnabled( false );
|
||||
|
||||
CollectionListModel<MPKeyPurpose> purposeModel = new CollectionListModel<>( MPKeyPurpose.values() );
|
||||
DocumentModel contextModel = new DocumentModel( new PlainDocument() );
|
||||
UnsignedIntegerModel counterModel = new UnsignedIntegerModel( UnsignedInteger.ONE );
|
||||
CollectionListModel<MPResultType> typeModel = new CollectionListModel<>( MPResultType.values() );
|
||||
DocumentModel stateModel = new DocumentModel( new PlainDocument() );
|
||||
|
||||
Runnable trigger = () -> Res.job( () -> {
|
||||
try {
|
||||
MPKeyPurpose purpose = purposeModel.getSelectedItem();
|
||||
MPResultType type = typeModel.getSelectedItem();
|
||||
|
||||
String result = ((purpose == null) || (type == null))? null:
|
||||
site.getResult( purpose, contextModel.getText(), counterModel.getNumber(), type, stateModel.getText() );
|
||||
|
||||
Res.ui( () -> resultField.setText( result ) );
|
||||
}
|
||||
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
|
||||
logger.err( e, "While computing site edit results." );
|
||||
}
|
||||
} );
|
||||
|
||||
purposeModel.selection( MPKeyPurpose.Authentication, p -> trigger.run() );
|
||||
contextModel.selection( c -> trigger.run() );
|
||||
counterModel.selection( c -> trigger.run() );
|
||||
typeModel.selection( MPResultType.DeriveKey, t -> {
|
||||
switch (t) {
|
||||
case DeriveKey:
|
||||
stateModel.setText( Integer.toString( site.getAlgorithm().mpw_keySize_min() ) );
|
||||
break;
|
||||
|
||||
default:
|
||||
stateModel.setText( null );
|
||||
}
|
||||
|
||||
trigger.run();
|
||||
} );
|
||||
stateModel.selection( c -> trigger.run() );
|
||||
|
||||
if (JOptionPane.OK_OPTION == Components.showDialog( this, site.getSiteName(), new JOptionPane( Components.panel(
|
||||
BoxLayout.PAGE_AXIS,
|
||||
Components.heading( "Key Calculator" ),
|
||||
Components.label( "Purpose:" ),
|
||||
Components.comboBox( purposeModel, MPKeyPurpose::getShortName ),
|
||||
Components.strut(),
|
||||
Components.label( "Context:" ),
|
||||
Components.textField( contextModel.getDocument() ),
|
||||
Components.label( "Counter:" ),
|
||||
Components.spinner( counterModel ),
|
||||
Components.label( "Type:" ),
|
||||
Components.comboBox( typeModel, this::getTypeDescription ),
|
||||
Components.label( "State:" ),
|
||||
Components.scrollPane( Components.textField( stateModel.getDocument() ) ),
|
||||
Components.strut(),
|
||||
resultField ) ) {
|
||||
{
|
||||
setOptions( new Object[]{ "Copy", "Cancel" } );
|
||||
setInitialValue( getOptions()[0] );
|
||||
}
|
||||
} ))
|
||||
copyResult( resultField.getText() );
|
||||
}
|
||||
|
||||
public void deleteSite() {
|
||||
MPSite<?> site = sitesModel.getSelectedItem();
|
||||
if (site == null)
|
||||
return;
|
||||
|
||||
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
|
||||
this, strf( "<html>Forget the site <strong>%s</strong>?</html>", site.getSiteName() ),
|
||||
"Delete Site", JOptionPane.YES_NO_OPTION ))
|
||||
user.deleteSite( site );
|
||||
}
|
||||
|
||||
private String getSiteDescription(@Nonnull final MPSite<?> site) {
|
||||
if (site instanceof MPNewSite)
|
||||
return strf( "<html><strong>%s</strong> <Add new site></html>", queryField.getText() );
|
||||
|
||||
ImmutableList.Builder<Object> parameters = ImmutableList.builder();
|
||||
try {
|
||||
MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null;
|
||||
if (fileSite != null)
|
||||
parameters.add( Res.format( fileSite.getLastUsed() ) );
|
||||
parameters.add( site.getAlgorithm().version() );
|
||||
parameters.add( strf( "#%d", site.getCounter().longValue() ) );
|
||||
parameters.add( strf( "<em>%s</em>", site.getLogin() ) );
|
||||
if ((fileSite != null) && (fileSite.getUrl() != null))
|
||||
parameters.add( fileSite.getUrl() );
|
||||
}
|
||||
catch (final MPAlgorithmException | MPKeyUnavailableException e) {
|
||||
logger.err( e, "While generating site description." );
|
||||
parameters.add( e.getLocalizedMessage() );
|
||||
}
|
||||
|
||||
return strf( "<html><strong>%s</strong> (%s)</html>", site.getSiteName(),
|
||||
Joiner.on( " - " ).skipNulls().join( parameters.build() ) );
|
||||
}
|
||||
|
||||
private void useSite(final ActionEvent event) {
|
||||
MPSite<?> site = sitesModel.getSelectedItem();
|
||||
if (site instanceof MPNewSite) {
|
||||
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
|
||||
this, strf( "<html>Remember the site <strong>%s</strong>?</html>", site.getSiteName() ),
|
||||
"New Site", JOptionPane.YES_NO_OPTION )) {
|
||||
sitesModel.setSelectedItem( user.addSite( site.getSiteName() ) );
|
||||
useSite( event );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
boolean loginResult = (copyLoginKeyStroke.getModifiers() & event.getModifiers()) != 0;
|
||||
showSiteResult( site, loginResult, result -> {
|
||||
if (result == null)
|
||||
return;
|
||||
|
||||
if (site instanceof MPFileSite)
|
||||
((MPFileSite) site).use();
|
||||
|
||||
copyResult( result );
|
||||
} );
|
||||
}
|
||||
|
||||
private void showSiteResult(@Nullable final MPSite<?> site) {
|
||||
showSiteResult( site, false, result -> {
|
||||
} );
|
||||
}
|
||||
|
||||
private void showSiteResult(@Nullable final MPSite<?> site, final boolean loginResult, final Consumer<String> resultCallback) {
|
||||
Res.job( () -> {
|
||||
try {
|
||||
if (site != null)
|
||||
return loginResult? site.getLogin(): site.getResult();
|
||||
}
|
||||
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
|
||||
logger.err( e, "While resolving password for: %s", site );
|
||||
}
|
||||
|
||||
return null;
|
||||
}, resultCallback.andThen( result -> Res.ui( () -> {
|
||||
passwordLabel.setText( ((result != null) && (site != null))? strf( "Your password for %s:", site.getSiteName() ): " " );
|
||||
passwordField.setText( (result != null)? result: " " );
|
||||
settingsButton.setEnabled( result != null );
|
||||
questionsButton.setEnabled( result != null );
|
||||
editButton.setEnabled( result != null );
|
||||
keyButton.setEnabled( result != null );
|
||||
deleteButton.setEnabled( result != null );
|
||||
} ) ) );
|
||||
}
|
||||
|
||||
private void useQuestion(@Nullable final MPQuestion question) {
|
||||
if (question instanceof MPNewQuestion) {
|
||||
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
|
||||
this,
|
||||
strf( "<html>Remember the security question with keyword <strong>%s</strong>?</html>",
|
||||
Strings.isNullOrEmpty( question.getKeyword() )? "<empty>": question.getKeyword() ),
|
||||
"New Question", JOptionPane.YES_NO_OPTION )) {
|
||||
useQuestion( question.getSite().addQuestion( question.getKeyword() ) );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
showQuestionResult( question, result -> {
|
||||
if (result == null)
|
||||
return;
|
||||
|
||||
if (question instanceof MPFileQuestion)
|
||||
((MPFileQuestion) question).use();
|
||||
|
||||
copyResult( result );
|
||||
} );
|
||||
}
|
||||
|
||||
private void showQuestionResult(@Nullable final MPQuestion question) {
|
||||
showQuestionResult( question, answer -> {
|
||||
} );
|
||||
}
|
||||
|
||||
private void showQuestionResult(@Nullable final MPQuestion question, final Consumer<String> resultCallback) {
|
||||
Res.job( () -> {
|
||||
try {
|
||||
if (question != null)
|
||||
return question.getAnswer();
|
||||
}
|
||||
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
|
||||
logger.err( e, "While resolving answer for: %s", question );
|
||||
}
|
||||
|
||||
return null;
|
||||
}, resultCallback.andThen( answer -> Res.ui( () -> {
|
||||
if ((answer == null) || (question == null))
|
||||
answerLabel.setText( " " );
|
||||
else
|
||||
answerLabel.setText(
|
||||
Strings.isNullOrEmpty( question.getKeyword() )?
|
||||
strf( "<html>Answer for site <b>%s</b>:", question.getSite().getSiteName() ):
|
||||
strf( "<html>Answer for site <b>%s</b>, of question with keyword <b>%s</b>:",
|
||||
question.getSite().getSiteName(), question.getKeyword() ) );
|
||||
answerField.setText( (answer != null)? answer: " " );
|
||||
} ) ) );
|
||||
}
|
||||
|
||||
private void copyResult(final String result) {
|
||||
Transferable clipboardContents = new StringSelection( result );
|
||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null );
|
||||
|
||||
Res.ui( () -> {
|
||||
Window answerDialog = SwingUtilities.windowForComponent( answerField );
|
||||
if (answerDialog instanceof Dialog)
|
||||
answerDialog.setVisible( false );
|
||||
|
||||
Window window = SwingUtilities.windowForComponent( UserContentPanel.this );
|
||||
if (window instanceof Frame)
|
||||
((Frame) window).setExtendedState( Frame.ICONIFIED );
|
||||
} );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyTyped(final KeyEvent event) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyPressed(final KeyEvent event) {
|
||||
if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN))
|
||||
sitesList.dispatchEvent( event );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyReleased(final KeyEvent event) {
|
||||
if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN))
|
||||
sitesList.dispatchEvent( event );
|
||||
}
|
||||
|
||||
private synchronized void updateSites(@Nullable final String query) {
|
||||
if (updateSitesJob != null)
|
||||
updateSitesJob.cancel( true );
|
||||
|
||||
updateSitesJob = Res.job( () -> {
|
||||
Collection<MPSite<?>> sites = new LinkedList<>( user.findSites( query ) );
|
||||
|
||||
if (!Strings.isNullOrEmpty( query ))
|
||||
if (sites.stream().noneMatch( site -> site.getSiteName().equalsIgnoreCase( query ) ))
|
||||
sites.add( new MPNewSite( user, query ) );
|
||||
|
||||
Res.ui( () -> sitesModel.set( sites ) );
|
||||
} );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserUpdated(final MPUser<?> user) {
|
||||
updateSites( queryField.getText() );
|
||||
showSiteResult( sitesModel.getSelectedItem() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserAuthenticated(final MPUser<?> user) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserInvalidated(final MPUser<?> user) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.7 KiB |