2
0

Compare commits

..

55 Commits

Author SHA1 Message Date
Maarten Billemont
554c0129a2 Pearl API update. 2018-10-15 18:09:46 -04:00
Maarten Billemont
78956beb08 2.7-java-10 2018-10-15 17:54:23 -04:00
Maarten Billemont
39dacc8e5a User config in _ext_mpw, global config.json & residence config.
Moved user preferences (default type & hide passwords) into _ext_mpw.
Fixed an issue with JSON serialization of any values.
Made update check & background residency globally configurable
preferences saved in config.json.
2018-10-15 02:21:30 -04:00
Maarten Billemont
34042e5462 Extract user preferences into a separate class. 2018-10-13 18:07:24 -04:00
Maarten Billemont
0a386d6fad Require JDK 10 or 9, gradle fails for JDK 11 currently. 2018-10-13 17:14:20 -04:00
Maarten Billemont
ff17a1d637 Fix file import & sane format migration. 2018-10-13 16:58:04 -04:00
Maarten Billemont
46fe919476 Centralize query searching. 2018-10-13 16:54:49 -04:00
Maarten Billemont
8f35ac5f64 Improved libxml2 cflags/ldflags. 2018-09-26 00:43:23 -04:00
Maarten Billemont
06100510c3 2.7-java-9 2018-09-26 00:03:36 -04:00
Maarten Billemont
1bf6109038 Hide passwords option & fix settings for new sites. 2018-09-26 00:00:05 -04:00
Maarten Billemont
f2fa2a25b2 Pass font resolution through the GraphicsEnvironment. 2018-09-25 20:55:04 -04:00
Maarten Billemont
2a0cfd3a32 Documentation and interface tweaks. 2018-09-22 19:43:13 -04:00
Maarten Billemont
3070967d34 Convenience flag for building a debuggable binary. 2018-09-22 14:22:17 -04:00
Maarten Billemont
e4837a284a Fall back to getline if ncurses cannot be initialized (eg. TERM not set). 2018-09-22 14:14:18 -04:00
Maarten Billemont
06ebe954f1 Clarify the interface a bit. 2018-09-22 14:13:00 -04:00
Maarten Billemont
48d4668575 Ensure all read utilities yield constant string pointers for safety. 2018-09-22 14:12:53 -04:00
Maarten Billemont
af768329a3 2.7-java-8 2018-09-13 22:38:45 -04:00
Maarten Billemont
9a04c28054 Fix initialization of text consumers & action handlers on sites list. 2018-09-13 22:37:28 -04:00
Maarten Billemont
ec9c55ec4d Access login names by holding shift. 2018-09-13 15:49:58 -04:00
Maarten Billemont
d8a735e1b1 Improve render speed of lists. 2018-09-13 15:49:42 -04:00
Maarten Billemont
a1eee88a54 Improved search query support. 2018-09-12 13:12:10 -04:00
Maarten Billemont
ac5286853a Write release JARs to site directory. 2018-08-28 02:43:24 -04:00
Maarten Billemont
39f6893742 2.7-java-7 2018-08-27 18:24:46 -04:00
Maarten Billemont
7bf7b8981c Fix duplication of user names in files list. 2018-08-27 18:24:00 -04:00
Maarten Billemont
09abe21fed Release masterpassword-gui-2.7.6 2018-08-27 13:22:03 -04:00
Maarten Billemont
6fae0fe425 2.7-java-6 2018-08-27 13:11:32 -04:00
Maarten Billemont
0558176847 Fix dependencies in native mpw DLLs. 2018-08-27 13:07:38 -04:00
Maarten Billemont
c553201cda Small site update. 2018-08-26 20:41:27 -04:00
Maarten Billemont
665be9494b Release masterpassword-gui-2.7.5 2018-08-26 18:19:01 -04:00
Maarten Billemont
5ca81b4aa7 Don't use daemon when setting release passwords in environment. 2018-08-25 11:57:36 -04:00
Maarten Billemont
3cbb063926 Refactor Native to try and load other architectures. 2018-08-24 16:33:14 -04:00
Maarten Billemont
d5551c8c8c Key calculator and access to the full algorithm. 2018-08-24 13:48:53 -04:00
Maarten Billemont
9a40e52d53 2.7-java-5 2018-08-19 16:13:39 -04:00
Maarten Billemont
6f0d768e69 Saving custom passwords and logins. 2018-08-19 16:11:43 -04:00
Maarten Billemont
40fdc8d248 Fix initialization dependency cycle & load files on init. 2018-08-18 13:43:41 -04:00
Maarten Billemont
6b9e1b8cb8 Standard label font & fix warnings. 2018-08-14 12:06:04 -04:00
Maarten Billemont
f41cdb8742 Site security questions and copy login name. 2018-08-13 17:53:31 -04:00
Maarten Billemont
10c6d203b8 Implement security answers & immediate site lookup. 2018-08-07 00:07:16 -04:00
Maarten Billemont
7d1aa9c9f4 Release 2.7.4 2018-08-02 12:34:24 -04:00
Maarten Billemont
c26281e3b7 2.7-java-4 2018-08-02 12:20:45 -04:00
Maarten Billemont
f0b1f0c9e0 Build for older glibc. 2018-08-02 12:19:49 -04:00
Maarten Billemont
9682efc7c9 Release masterpassword-gui-2.7.3 2018-08-02 01:44:50 -04:00
Maarten Billemont
1264cad377 2.7-java-3 2018-08-02 01:37:37 -04:00
Maarten Billemont
d185a0af14 Add mpw native binary for windows 32-bit. 2018-08-02 01:37:10 -04:00
Maarten Billemont
4275a6cc61 Fix build on Windows. 2018-08-02 01:32:55 -04:00
Maarten Billemont
c94ff429e8 Switch linux build of libmpw to debian for glibc instead of musl libc. 2018-08-01 20:13:42 -04:00
Maarten Billemont
00744cb264 Statically link the mpw library. 2018-08-01 14:20:47 -04:00
Maarten Billemont
7202fe6d1d Bump site for release of masterpassword-gui-2.7.2.jar 2018-07-31 15:35:57 -04:00
Maarten Billemont
63b4d9cd2e 2.7-java-2 2018-07-31 15:32:13 -04:00
Maarten Billemont
36a7c7f423 Clean up iconifying on copy. 2018-07-31 15:31:47 -04:00
Maarten Billemont
c2c4fb18bf Help improvements. 2018-07-31 15:16:33 -04:00
Maarten Billemont
3fc8acba70 Global hotkey, iconifying and application activation, help text. 2018-07-31 14:55:19 -04:00
Maarten Billemont
f5c0c4d787 Fix offsetting local time back to UTC. 2018-07-31 12:44:49 -04:00
Maarten Billemont
86775f1c75 Standardize epoch time calculation. 2018-07-31 09:27:41 -04:00
Maarten Billemont
2bb190f49a Bump site for masterpassword-gui-2.7.1 release. 2018-07-29 15:38:54 -04:00
86 changed files with 2215 additions and 837 deletions

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@ Thumbs.db
*.iml *.iml
*.ipr *.ipr
*.iws *.iws
out
# Xcode IDE # Xcode IDE
xcuserdata/ xcuserdata/

View File

@@ -9,7 +9,7 @@ build_project:
- "( ./lib/bin/build_libsodium-macos clean && ./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 )" - "( ./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 ./platform-independent/c/cli && ./clean && targets=all ./build && ./mpw-tests && ./mpw-cli-tests )"
- "( cd ./gradle && ./gradlew --stacktrace clean test )" - "( export JAVA_HOME=$(java_home -Fv 10 || java_home -Fv 9* ) && 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 iOS' -sdk iphonesimulator clean build )"
- "( xcodebuild -workspace platform-darwin/MasterPassword.xcworkspace -configuration 'Test' -scheme 'MasterPassword macOS' clean build )" - "( xcodebuild -workspace platform-darwin/MasterPassword.xcworkspace -configuration 'Test' -scheme 'MasterPassword macOS' clean build )"
tags: tags:

View File

@@ -1,9 +1,12 @@
FROM alpine FROM debian:stable-slim
# For i386 # For i386
#FROM i386/alpine #FROM i386/debian:stable-slim
#ENTRYPOINT ["linux32", "--"] #ENTRYPOINT ["linux32", "--"]
RUN apk add --no-cache git libtool automake autoconf make g++ bash openjdk8 # 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 git clone --depth=3 $(: --shallow-submodules) --recurse-submodules https://gitlab.com/MasterPassword/MasterPassword.git /mpw
RUN cd /mpw/gradle && ./gradlew -i clean build RUN cd /mpw/gradle && ./gradlew -i clean build

14
gradle/.idea/misc.xml generated
View File

@@ -5,21 +5,31 @@
<option name="myDefaultNotNull" value="javax.annotation.Nonnull" /> <option name="myDefaultNotNull" value="javax.annotation.Nonnull" />
<option name="myNullables"> <option name="myNullables">
<value> <value>
<list size="4"> <list size="9">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" /> <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="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="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="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> </list>
</value> </value>
</option> </option>
<option name="myNotNulls"> <option name="myNotNulls">
<value> <value>
<list size="4"> <list size="9">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" /> <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="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="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="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> </list>
</value> </value>
</option> </option>

View File

@@ -2,11 +2,11 @@ To build a release distribution:
Desktop: 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: 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: Note:

View File

@@ -2,7 +2,7 @@ allprojects {
apply plugin: 'findbugs' apply plugin: 'findbugs'
group = 'com.lyndir.masterpassword' group = 'com.lyndir.masterpassword'
version = '2.7.1' version = '2.7.10'
tasks.withType( JavaCompile ) { tasks.withType( JavaCompile ) {
options.encoding = 'UTF-8' options.encoding = 'UTF-8'

View File

@@ -32,8 +32,15 @@ PATH+=:/usr/local/bin
needs() { _needs "$@"; } needs() { _needs "$@"; }
_needs() { _needs() {
local failed=0 local failed=0
for tool; do for spec; do
hash "$tool" || { echo >&2 "Missing: $tool. Please install this tool."; (( failed++ )); } 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 done
return $failed return $failed
@@ -51,15 +58,15 @@ _initialize() {
# #
# Check if all tools needed for the default implementations are available. # Check if all tools needed for the default implementations are available.
# #
# By default, this will check for `libtool` (for libtoolize), `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() { _initialize_needs "$@"; }
_initialize_needs() { _initialize_needs() {
if [[ $platform = windows ]]; then if [[ $platform = windows ]]; then
needs cmd needs cmd
export VSINSTALLDIR="${VSINSTALLDIR:-$(cd "$(cygpath -F 0x002a)/Microsoft Visual Studio"/*/*/Common7/.. && pwd)}" 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'."; return 1; } [[ -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 else
needs libtool automake autoconf needs libtool:libtoolize,glibtoolize automake autoconf make
fi fi
} }
@@ -195,7 +202,7 @@ _target_build() {
if [[ $platform = windows ]]; then if [[ $platform = windows ]]; then
# I cannot for the life of me figure out how to pass this command directly into cmd. # 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=ReleaseDLL;Platform=%s;OutDir=%s' "$arch" "$(cygpath -w "${prefix##$PWD/}/$arch/")" > .build.bat 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 cmd //c .build.bat
rm -f .build.bat rm -f .build.bat
else else
@@ -271,7 +278,10 @@ _finalize_merge() {
# By default, this will run `make clean`. # By default, this will run `make clean`.
finalize_clean() { _finalize_clean "$@"; } finalize_clean() { _finalize_clean "$@"; }
_finalize_clean() { _finalize_clean() {
if [[ $platform = windows ]]; then :
else
[[ ! -e Makefile ]] || make -s clean [[ ! -e Makefile ]] || make -s clean
fi
} }
# build <name> [<platform>] # build <name> [<platform>]

View File

@@ -1,4 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
source "${BASH_SOURCE%/*}/build_lib" 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 build libsodium windows

View File

@@ -3917,7 +3917,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = "/bin/sh -e"; shellPath = "/bin/sh -e";
shellScript = "exec Scripts/genassets"; shellScript = "exec Scripts/genassets\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
DAB7AE4C1F3D56AD00C856B1 /* ShellScript */ = { DAB7AE4C1F3D56AD00C856B1 /* ShellScript */ = {

View File

@@ -120,21 +120,21 @@
MPSiteEntity *site = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]]; MPSiteEntity *site = [self siteInContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]];
if (indexPath.section == 0) { if (indexPath.section == 0) {
if (indexPath.item == 0) { if (indexPath.item == 0) {
MPGlobalAnswersCell *cell = [MPGlobalAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath]; MPGlobalAnswersCell *cell = [MPGlobalAnswersCell dequeueFromTableView:tableView indexPath:indexPath];
[cell setSite:site]; [cell setSite:site];
return cell; return cell;
} }
if (indexPath.item == 1) if (indexPath.item == 1)
return [MPSendAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath]; return [MPSendAnswersCell dequeueFromTableView:tableView indexPath:indexPath];
if (indexPath.item == 2) { if (indexPath.item == 2) {
MPMultipleAnswersCell *cell = [MPMultipleAnswersCell dequeueCellFromTableView:tableView indexPath:indexPath]; MPMultipleAnswersCell *cell = [MPMultipleAnswersCell dequeueFromTableView:tableView indexPath:indexPath];
cell.accessoryType = self.multiple? UITableViewCellAccessoryCheckmark: UITableViewCellAccessoryNone; cell.accessoryType = self.multiple? UITableViewCellAccessoryCheckmark: UITableViewCellAccessoryNone;
return cell; return cell;
} }
Throw( @"Unsupported row index: %@", indexPath ); Throw( @"Unsupported row index: %@", indexPath );
} }
MPAnswersQuestionCell *cell = [MPAnswersQuestionCell dequeueCellFromTableView:tableView indexPath:indexPath]; MPAnswersQuestionCell *cell = [MPAnswersQuestionCell dequeueFromTableView:tableView indexPath:indexPath];
MPSiteQuestionEntity *question = nil; MPSiteQuestionEntity *question = nil;
if ([site.questions count] > indexPath.item) if ([site.questions count] > indexPath.item)
question = site.questions[indexPath.item]; question = site.questions[indexPath.item];

View File

@@ -150,7 +150,7 @@
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MPGuideStepCell *cell = [MPGuideStepCell dequeueCellFromCollectionView:collectionView indexPath:indexPath]; MPGuideStepCell *cell = [MPGuideStepCell dequeueFromCollectionView:collectionView indexPath:indexPath];
cell.imageView.image = ((MPGuideStep *)self.steps[indexPath.item]).image; cell.imageView.image = ((MPGuideStep *)self.steps[indexPath.item]).image;
cell.contentView.frame = cell.bounds; cell.contentView.frame = cell.bounds;

View File

@@ -179,7 +179,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MPSiteCell *cell = [MPSiteCell dequeueCellFromCollectionView:collectionView indexPath:indexPath]; MPSiteCell *cell = [MPSiteCell dequeueFromCollectionView:collectionView indexPath:indexPath];
[cell setFuzzyGroups:self.fuzzyGroups]; [cell setFuzzyGroups:self.fuzzyGroups];
id item = self.dataSource[(NSUInteger)indexPath.section][(NSUInteger)indexPath.item]; id item = self.dataSource[(NSUInteger)indexPath.section][(NSUInteger)indexPath.item];
if ([item isKindOfClass:[MPSiteEntity class]]) if ([item isKindOfClass:[MPSiteEntity class]])

View File

@@ -102,9 +102,9 @@ PearlEnum( MPDevelopmentFuelConsumption,
SKProduct *product = content; SKProduct *product = content;
MPStoreProductCell *cell; MPStoreProductCell *cell;
if ([product.productIdentifier isEqualToString:MPProductFuel]) if ([product.productIdentifier isEqualToString:MPProductFuel])
cell = [MPStoreFuelProductCell dequeueCellFromTableView:tableView indexPath:indexPath]; cell = [MPStoreFuelProductCell dequeueFromTableView:tableView indexPath:indexPath];
else else
cell = [MPStoreProductCell dequeueCellFromTableView:tableView indexPath:indexPath]; cell = [MPStoreProductCell dequeueFromTableView:tableView indexPath:indexPath];
[cell updateWithProduct:product transaction:self.transactions[product.productIdentifier]]; [cell updateWithProduct:product transaction:self.transactions[product.productIdentifier]];
return cell; return cell;

View File

@@ -1,10 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# USAGE # USAGE
# [targets='...'] [mpw_feature=0|1 ...] [CFLAGS='...'] [LDFLAGS='...'] ./build [cc arguments ...] # [targets='...'] [mpw_feature=0|1 ...] [CFLAGS='...'] [LDFLAGS='...'] ./build [-v|-d|-h|--] [cc arguments ...]
# #
# By default, you should only need to run ./build # By default, you should only need to run ./build
# #
# -v: verbose mode, outputs state information and compiler commands.
# -d: debug build, modifies default build flags to produce binaries best suited for debugging.
# -h: show this usage information.
#
# You can customize the targets that are built using targets='...'. Use targets='all' to build all targets. # You can customize the targets that are built using targets='...'. Use targets='all' to build all targets.
# By default, we only build the 'mpw' target. # By default, we only build the 'mpw' target.
# See targets_all for all possible targets as well as the features they support and require. # See targets_all for all possible targets as well as the features they support and require.
@@ -27,29 +31,53 @@ set -e
### CONFIGURATION ### CONFIGURATION
# Targets to build. verbose=0
# Options
while getopts :vdh opt; do
case $opt in
v) verbose=1 ;;
d) debug=1 ;;
h|?) sed -n '/^[^#]/q;p' "${BASH_SOURCE##*/}"; exit ;;
esac
done
shift "$(( OPTIND - 1 ))"
# Targets to build
targets_all=( targets_all=(
mpw # C CLI version of Master Password (needs: mpw_sodium, optional: mpw_color, mpw_json). mpw # C CLI version of Master Password (needs: mpw_sodium, optional: mpw_color, mpw_json).
mpw-bench # C CLI Master Password benchmark utility (needs: mpw_sodium). mpw-bench # C CLI Master Password benchmark utility (needs: mpw_sodium).
mpw-tests # C Master Password algorithm test suite (needs: mpw_sodium, mpw_xml). mpw-tests # C Master Password algorithm test suite (needs: mpw_sodium, mpw_xml).
) )
targets_default='mpw' # Override with: targets='...' ./build targets_default='mpw' # Override with: targets='...' ./build
targets=${targets[*]:-$targets_default}
# Features. # Features
mpw_sodium=${mpw_sodium:-1} # Implement crypto functions with sodium (depends on libsodium). mpw_sodium=${mpw_sodium:-1} # Implement crypto functions with sodium (depends on libsodium).
mpw_json=${mpw_json:-1} # Support JSON-based user configuration format (depends on libjson-c). mpw_json=${mpw_json:-1} # Support JSON-based user configuration format (depends on libjson-c).
mpw_color=${mpw_color:-1} # Colorized identicon (depends on libncurses). mpw_color=${mpw_color:-1} # Colorized identicon (depends on libncurses).
mpw_xml=${mpw_xml:-1} # XML parsing (depends on libxml2). mpw_xml=${mpw_xml:-1} # XML parsing (depends on libxml2).
# Default build flags. # Default build flags
cflags=( -O3 $CFLAGS ) cflags=( -O3 $CFLAGS ); unset CFLAGS
ldflags=( $LDFLAGS ) ldflags=( $LDFLAGS ); unset LDFLAGS
if (( debug )); then
cflags+=( -O0 -g )
fi
# Version. # Version
if { mpw_version=$(git describe --match '*-cli*' --long --dirty) || mpw_version=$(<VERSION); } 2>/dev/null; then if { mpw_version=$(git describe --match '*-cli*' --long --dirty) || mpw_version=$(<VERSION); } 2>/dev/null; then
cflags+=( -D"MP_VERSION=$mpw_version" ) cflags+=( -D"MP_VERSION=$mpw_version" )
fi fi
echo 2>&1 "Current mpw source version ${mpw_version:-<unknown>}..." echo "Current mpw source version ${mpw_version:-<unknown>}..."
# Meta
if (( verbose )); then
echo "mpw_sodium=${mpw_sodium}, mpw_json=${mpw_json}, mpw_color=${mpw_color}, mpw_xml=${mpw_xml}"
echo "CFLAGS: ${cflags[*]}"
echo "LDFLAGS: ${ldflags[*]}"
echo "targets: ${targets[*]}"
fi
### TARGET: MPW ### TARGET: MPW
@@ -132,18 +160,20 @@ mpw-tests() {
haslib() { haslib() {
cc -x c "${ldflags[@]}" -l"$1" -o /dev/null - <<< 'int main() { return 0; }' &>/dev/null cc -x c "${ldflags[@]}" -l"$1" -o /dev/null - <<< 'int main() { return 0; }' &>/dev/null
} }
cc() { cc() (
if hash llvm-gcc 2>/dev/null; then (( verbose )) && set -x
if { hash llvm-gcc; } 2>/dev/null; then
llvm-gcc "$@" llvm-gcc "$@"
elif hash gcc 2>/dev/null; then elif { hash gcc; } 2>/dev/null; then
gcc -std=c11 "$@" gcc -std=c11 "$@"
elif hash clang 2>/dev/null; then elif { hash clang; } 2>/dev/null; then
clang "$@" clang "$@"
else else
echo >&2 "Need a compiler. Please install GCC or LLVM." echo >&2 "Need a compiler. Please install GCC or LLVM."
exit 1 exit 1
fi fi
} )
### DEPENDENCIES ### DEPENDENCIES
@@ -156,7 +186,7 @@ use() {
for lib in "$lib" "$@"; do for lib in "$lib" "$@"; do
haslib "$lib" && ldflags+=( -l"$lib" ) haslib "$lib" && ldflags+=( -l"$lib" )
done done
echo >&2 "INFO: Enabled $option (lib$lib)." echo "INFO: Enabled $option (lib$lib)."
return 0 return 0
elif [[ $requisite == required ]]; then elif [[ $requisite == required ]]; then
@@ -174,7 +204,7 @@ use() {
exit 1 exit 1
else else
echo >&2 "INFO: $option is supported but not enabled." echo "INFO: $option is supported but not enabled."
return 1 return 1
fi fi
} }
@@ -192,13 +222,13 @@ use_mpw_json() {
} }
use_mpw_xml() { use_mpw_xml() {
local requisite=$1 local requisite=$1
use mpw_xml "$requisite" xml2 && cflags+=( -D"MPW_XML=1" -I"/usr/include/libxml2" -I"/usr/local/include/libxml2" ) ||: use mpw_xml "$requisite" xml2 && cflags+=( $(xml2-config --cflags) ) ldflags+=( $(xml2-config --libs) ) ||:
} }
### BUILD TARGETS ### BUILD TARGETS
for target in "${targets_all[@]}"; do for target in "${targets_all[@]}"; do
if [[ ${targets:-$targets_default} == 'all' || " ${targets:-$targets_default} " = *" $target "* ]]; then if [[ $targets == 'all' || " $targets " = *" $target "* ]]; then
echo echo
echo "Building target: $target..." echo "Building target: $target..."
( "$target" "$@" ) ( "$target" "$@" )

View File

@@ -43,7 +43,7 @@ const char *mpw_getenv(const char *variableName) {
return envBuf? mpw_strdup( envBuf ): NULL; return envBuf? mpw_strdup( envBuf ): NULL;
} }
char *mpw_askpass(const char *prompt) { const char *mpw_askpass(const char *prompt) {
const char *askpass = mpw_getenv( MP_ENV_askpass ); const char *askpass = mpw_getenv( MP_ENV_askpass );
if (!askpass) if (!askpass)
@@ -74,7 +74,7 @@ char *mpw_askpass(const char *prompt) {
} }
close( pipes[1] ); close( pipes[1] );
char *answer = mpw_read_fd( pipes[0] ); const char *answer = mpw_read_fd( pipes[0] );
close( pipes[0] ); close( pipes[0] );
int status; int status;
if (waitpid( pid, &status, 0 ) == ERR) { if (waitpid( pid, &status, 0 ) == ERR) {
@@ -86,7 +86,7 @@ char *mpw_askpass(const char *prompt) {
if (WIFEXITED( status ) && WEXITSTATUS( status ) == EXIT_SUCCESS && answer && strlen( answer )) { if (WIFEXITED( status ) && WEXITSTATUS( status ) == EXIT_SUCCESS && answer && strlen( answer )) {
// Remove trailing newline. // Remove trailing newline.
if (answer[strlen( answer ) - 1] == '\n') if (answer[strlen( answer ) - 1] == '\n')
answer[strlen( answer ) - 1] = '\0'; mpw_replace_string( answer, mpw_strndup( answer, strlen( answer ) - 1 ) );
return answer; return answer;
} }
@@ -97,13 +97,14 @@ char *mpw_askpass(const char *prompt) {
static const char *_mpw_getline(const char *prompt, bool silent) { static const char *_mpw_getline(const char *prompt, bool silent) {
// Get answer from askpass. // Get answer from askpass.
char *answer = mpw_askpass( prompt ); const char *answer = mpw_askpass( prompt );
if (answer) if (answer)
return answer; return answer;
#if MPW_COLOR #if MPW_COLOR
// Initialize a curses screen. // Initialize a curses screen.
SCREEN *screen = newterm( NULL, stderr, stdin ); SCREEN *screen = newterm( NULL, stderr, stdin );
if (screen) {
start_color(); start_color();
init_pair( 1, COLOR_WHITE, COLOR_BLUE ); init_pair( 1, COLOR_WHITE, COLOR_BLUE );
init_pair( 2, COLOR_BLACK, COLOR_WHITE ); init_pair( 2, COLOR_BLACK, COLOR_WHITE );
@@ -135,7 +136,8 @@ static const char *_mpw_getline(const char *prompt, bool silent) {
noecho(); noecho();
result = mvgetnstr( rows / 2 + 1, (cols - 1) / 2, str, MPW_MAX_INPUT ); result = mvgetnstr( rows / 2 + 1, (cols - 1) / 2, str, MPW_MAX_INPUT );
echo(); echo();
} else { }
else {
mvprintw( rows / 2 + 1, (cols - (MPW_MAX_INPUT + 2)) / 2, "%*s", MPW_MAX_INPUT + 2, "" ); mvprintw( rows / 2 + 1, (cols - (MPW_MAX_INPUT + 2)) / 2, "%*s", MPW_MAX_INPUT + 2, "" );
refresh(); refresh();
@@ -147,21 +149,22 @@ static const char *_mpw_getline(const char *prompt, bool silent) {
delscreen( screen ); delscreen( screen );
return result == ERR? NULL: mpw_strndup( str, MPW_MAX_INPUT ); return result == ERR? NULL: mpw_strndup( str, MPW_MAX_INPUT );
#else }
#endif
// Get password from terminal. // Get password from terminal.
fprintf( stderr, "%s ", prompt ); fprintf( stderr, "%s ", prompt );
size_t bufSize = 0; size_t bufSize = 0;
ssize_t lineSize = getline( &answer, &bufSize, stdin ); ssize_t lineSize = getline( (char **)&answer, &bufSize, stdin );
if (lineSize <= 1) { if (lineSize <= 1) {
mpw_free_string( &answer ); mpw_free_string( &answer );
return NULL; return NULL;
} }
// Remove trailing newline. // Remove trailing newline.
answer[lineSize - 1] = '\0'; mpw_replace_string( answer, mpw_strndup( answer, (size_t)lineSize - 1 ) );
return answer; return answer;
#endif
} }
const char *mpw_getline(const char *prompt) { const char *mpw_getline(const char *prompt) {
@@ -250,7 +253,7 @@ bool mpw_mkdirs(const char *filePath) {
return success; return success;
} }
char *mpw_read_fd(int fd) { const char *mpw_read_fd(int fd) {
char *buf = NULL; char *buf = NULL;
size_t blockSize = 4096, bufSize = 0, bufOffset = 0; size_t blockSize = 4096, bufSize = 0, bufOffset = 0;
@@ -263,7 +266,7 @@ char *mpw_read_fd(int fd) {
return buf; return buf;
} }
char *mpw_read_file(FILE *file) { const char *mpw_read_file(FILE *file) {
if (!file) if (!file)
return NULL; return NULL;

View File

@@ -36,7 +36,7 @@ const char *mpw_getenv(const char *variableName);
/** Use the askpass program to prompt the user. /** Use the askpass program to prompt the user.
* @return A newly allocated string or NULL if askpass is not supported or an error occurred. */ * @return A newly allocated string or NULL if askpass is not supported or an error occurred. */
char *mpw_askpass(const char *prompt); const char *mpw_askpass(const char *prompt);
/** Ask the user a question. /** Ask the user a question.
* @return A newly allocated string or NULL if an error occurred trying to read from the user. */ * @return A newly allocated string or NULL if an error occurred trying to read from the user. */
@@ -58,11 +58,11 @@ bool mpw_mkdirs(const char *filePath);
/** Read until EOF from the given file descriptor. /** Read until EOF from the given file descriptor.
* @return A newly allocated string or NULL the read buffer couldn't be allocated. */ * @return A newly allocated string or NULL the read buffer couldn't be allocated. */
char *mpw_read_fd(int fd); const char *mpw_read_fd(int fd);
/** Read the file contents of a given file. /** Read the file contents of a given file.
* @return A newly allocated string or NULL the read buffer couldn't be allocated. */ * @return A newly allocated string or NULL the read buffer couldn't be allocated. */
char *mpw_read_file(FILE *file); const char *mpw_read_file(FILE *file);
/** Encode a visual fingerprint for a user. /** Encode a visual fingerprint for a user.
* @return A newly allocated string. */ * @return A newly allocated string. */

View File

@@ -476,8 +476,8 @@ void cli_user(Arguments *args, Operation *operation) {
else { else {
// Read file. // Read file.
char *sitesInputData = mpw_read_file( sitesFile ); const char *sitesInputData = mpw_read_file( sitesFile );
if (ferror( sitesFile )) if (!sitesInputData || ferror( sitesFile ))
wrn( "Error while reading configuration file:\n %s: %d", operation->sitesPath, ferror( sitesFile ) ); wrn( "Error while reading configuration file:\n %s: %d", operation->sitesPath, ferror( sitesFile ) );
fclose( sitesFile ); fclose( sitesFile );

View File

@@ -35,7 +35,7 @@ library {
} }
withType( GccCompatibleToolChain ) { withType( GccCompatibleToolChain ) {
eachPlatform { eachPlatform {
cppCompiler.withArguments { addAll( ['-x', 'c', '-O3', '-std=c11', '-Werror', '-DMPW_SODIUM=1'] ) } cppCompiler.withArguments { addAll( ['-x', 'c', '-O3', '-Werror', '-DMPW_SODIUM=1'] ) }
} }
} }
} }
@@ -69,14 +69,14 @@ library {
} }
// libjson-c // libjson-c
archive.dependsOn project.tasks.maybeCreate( "build_libjson-c-${system}", Exec ).configure { /*archive.dependsOn project.tasks.maybeCreate( "build_libjson-c-${system}", Exec ).configure {
commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-${system}" commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-${system}"
privateHeaders.from "$rootDir/../lib/libjson-c/build-${system}~/out/include" privateHeaders.from "$rootDir/../lib/libjson-c/build-${system}~/out/include"
add( linkLibraries.name, fileTree( "$rootDir/../lib/libjson-c/build-${system}~/out/lib" ) ) add( linkLibraries.name, fileTree( "$rootDir/../lib/libjson-c/build-${system}~/out/lib" ) )
} }
clean.dependsOn project.tasks.maybeCreate( "clean_libjson-c-${system}", Exec ).configure { clean.dependsOn project.tasks.maybeCreate( "clean_libjson-c-${system}", Exec ).configure {
commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-${system}", 'clean' commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-${system}", 'clean'
} }*/
} }
} }
} }

Binary file not shown.

View File

@@ -7,7 +7,7 @@
// TODO: We may need to zero the jbytes safely. // 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; JNIEnv* env;
if ((*vm)->GetEnv( vm, (void **)&env, JNI_VERSION_1_6 ) != JNI_OK) if ((*vm)->GetEnv( vm, (void **)&env, JNI_VERSION_1_6 ) != JNI_OK)
return -1; return -1;

View File

@@ -35,10 +35,10 @@ char *mpw_get_token(const char **in, const char *eol, char *delim) {
return token; return token;
} }
time_t mpw_mktime( time_t mpw_timegm(const char *time) {
const char *time) {
// TODO: Support for parsing non-UTC time strings // TODO: Support for parsing non-UTC time strings
// Parse time as a UTC timestamp, into a tm.
struct tm tm = { .tm_isdst = -1 }; struct tm tm = { .tm_isdst = -1 };
if (time && sscanf( time, "%4d-%2d-%2dT%2d:%2d:%2dZ", if (time && sscanf( time, "%4d-%2d-%2dT%2d:%2d:%2dZ",
&tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
@@ -46,8 +46,10 @@ time_t mpw_mktime(
tm.tm_year -= 1900; // tm_year 0 = rfc3339 year 1900 tm.tm_year -= 1900; // tm_year 0 = rfc3339 year 1900
tm.tm_mon -= 1; // tm_mon 0 = rfc3339 month 1 tm.tm_mon -= 1; // tm_mon 0 = rfc3339 month 1
// mktime converts tm to local, setting tm_gmtoff; use it to offset the result back to UTC. // mktime interprets tm as being local, we need to offset back to UTC (timegm/tm_gmtoff are non-standard).
return mktime( &tm ) + tm.tm_gmtoff; 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; return false;

View File

@@ -34,7 +34,7 @@
char *mpw_get_token( char *mpw_get_token(
const char **in, const char *eol, char *delim); const char **in, const char *eol, char *delim);
/** Convert an RFC 3339 time string into epoch time. */ /** Convert an RFC 3339 time string into epoch time. */
time_t mpw_mktime( time_t mpw_timegm(
const char *time); const char *time);
/// JSON parsing. /// JSON parsing.

View File

@@ -407,7 +407,7 @@ static void mpw_marshal_read_flat_info(
if (strcmp( headerName, "Passwords" ) == 0) if (strcmp( headerName, "Passwords" ) == 0)
info->redacted = strcmp( headerValue, "VISIBLE" ) != 0; info->redacted = strcmp( headerValue, "VISIBLE" ) != 0;
if (strcmp( headerName, "Date" ) == 0) if (strcmp( headerName, "Date" ) == 0)
info->date = mpw_mktime( headerValue ); info->date = mpw_timegm( headerValue );
mpw_free_strings( &headerName, &headerValue, NULL ); mpw_free_strings( &headerName, &headerValue, NULL );
continue; continue;
@@ -580,7 +580,7 @@ static MPMarshalledUser *mpw_marshal_read_flat(
return NULL; return NULL;
} }
MPAlgorithmVersion siteAlgorithm = (MPAlgorithmVersion)value; MPAlgorithmVersion siteAlgorithm = (MPAlgorithmVersion)value;
time_t siteLastUsed = mpw_mktime( str_lastUsed ); time_t siteLastUsed = mpw_timegm( str_lastUsed );
if (!siteLastUsed) { if (!siteLastUsed) {
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid site last used: %s: %s", siteName, str_lastUsed ) }; *error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid site last used: %s: %s", siteName, str_lastUsed ) };
return NULL; return NULL;
@@ -650,7 +650,7 @@ static void mpw_marshal_read_json_info(
if (fileFormat < 1) if (fileFormat < 1)
return; return;
info->redacted = mpw_get_json_boolean( json_file, "export.redacted", true ); 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" // Section: "user"
info->algorithm = (MPAlgorithmVersion)mpw_get_json_int( json_file, "user.algorithm", MPAlgorithmVersionCurrent ); info->algorithm = (MPAlgorithmVersion)mpw_get_json_int( json_file, "user.algorithm", MPAlgorithmVersionCurrent );
@@ -707,7 +707,7 @@ static MPMarshalledUser *mpw_marshal_read_json(
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid user default type: %u", defaultType ) }; *error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid user default type: %u", defaultType ) };
return NULL; return NULL;
} }
time_t lastUsed = mpw_mktime( str_lastUsed ); time_t lastUsed = mpw_timegm( str_lastUsed );
if (!lastUsed) { if (!lastUsed) {
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid user last used: %s", str_lastUsed ) }; *error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid user last used: %s", str_lastUsed ) };
return NULL; return NULL;
@@ -760,7 +760,7 @@ static MPMarshalledUser *mpw_marshal_read_json(
MPResultType siteLoginType = (MPResultType)mpw_get_json_int( json_site.val, "login_type", MPResultTypeTemplateName ); 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 ); 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 ); 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) { if (!siteLastUsed) {
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid site last used: %s: %s", siteName, str_lastUsed ) }; *error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid site last used: %s: %s", siteName, str_lastUsed ) };
return NULL; return NULL;

View File

@@ -115,6 +115,8 @@ bool mpw_string_pushf(
/** Push an integer onto a buffer. reallocs the given buffer and appends the given integer. */ /** Push an integer onto a buffer. reallocs the given buffer and appends the given integer. */
bool mpw_push_int( bool mpw_push_int(
uint8_t **buffer, size_t *bufferSize, const uint32_t pushInt); uint8_t **buffer, size_t *bufferSize, const uint32_t pushInt);
// These defines merely exist to force the void** cast (& do type-checking), since void** casts are not automatic.
/** Reallocate the given buffer from the given size by adding the delta size. /** 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 * 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. * and the buffer pointer may be updated to a new memory address.
@@ -124,26 +126,26 @@ bool mpw_push_int(
* @param deltaSize The amount to increase the buffer's size by. * @param deltaSize The amount to increase the buffer's size by.
* @return true if successful, false if reallocation failed. * @return true if successful, false if reallocation failed.
*/ */
#define mpw_realloc(buffer, bufferSize, deltaSize) \ #define mpw_realloc( \
/* const void** */buffer, /* size_t* */bufferSize, /* const size_t */deltaSize) \
({ __typeof__(buffer) _b = buffer; const void *__b = *_b; (void)__b; __mpw_realloc( (const void **)_b, bufferSize, deltaSize ); }) ({ __typeof__(buffer) _b = buffer; const void *__b = *_b; (void)__b; __mpw_realloc( (const void **)_b, bufferSize, deltaSize ); })
bool __mpw_realloc(const void **buffer, size_t *bufferSize, const size_t deltaSize);
void mpw_zero(
void *buffer, size_t bufferSize);
/** Free a buffer after zero'ing its contents, then set the reference to NULL. */ /** Free a buffer after zero'ing its contents, then set the reference to NULL. */
#define mpw_free(buffer, bufferSize) \ #define mpw_free( \
/* void** */buffer, /* size_t */ bufferSize) \
({ __typeof__(buffer) _b = buffer; const void *__b = *_b; (void)__b; __mpw_free( (void **)_b, bufferSize ); }) ({ __typeof__(buffer) _b = buffer; const void *__b = *_b; (void)__b; __mpw_free( (void **)_b, bufferSize ); })
bool __mpw_free(
void **buffer, size_t bufferSize);
/** Free a string after zero'ing its contents, then set the reference to NULL. */ /** Free a string after zero'ing its contents, then set the reference to NULL. */
#define mpw_free_string(string) \ #define mpw_free_string( \
/* char** */string) \
({ __typeof__(string) _s = string; const char *__s = *_s; (void)__s; __mpw_free_string( (char **)_s ); }) ({ __typeof__(string) _s = string; const char *__s = *_s; (void)__s; __mpw_free_string( (char **)_s ); })
bool __mpw_free_string(
char **string);
/** Free strings after zero'ing their contents, then set the references to NULL. Terminate the va_list with NULL. */ /** Free strings after zero'ing their contents, then set the references to NULL. Terminate the va_list with NULL. */
#define mpw_free_strings(strings, ...) \ #define mpw_free_strings( \
/* char** */strings, ...) \
({ __typeof__(strings) _s = strings; const char *__s = *_s; (void)__s; __mpw_free_strings( (char **)_s, __VA_ARGS__ ); }) ({ __typeof__(strings) _s = strings; const char *__s = *_s; (void)__s; __mpw_free_strings( (char **)_s, __VA_ARGS__ ); })
bool __mpw_free_strings( /** Free a string after zero'ing its contents, then set the reference to the replacement string.
char **strings, ...); * The replacement string is generated before the original is freed; it may be a derivative of the original. */
#define mpw_replace_string( \
/* char* */string, /* char* */replacement) \
do { const char *replacement_ = replacement; mpw_free_string( &string ); string = replacement_; } while (0)
#ifdef _MSC_VER #ifdef _MSC_VER
#undef mpw_realloc #undef mpw_realloc
#define mpw_realloc(buffer, bufferSize, deltaSize) \ #define mpw_realloc(buffer, bufferSize, deltaSize) \
@@ -158,6 +160,16 @@ bool __mpw_free_strings(
#define mpw_free_strings(strings, ...) \ #define mpw_free_strings(strings, ...) \
__mpw_free_strings( (char **)strings, __VA_ARGS__ ) __mpw_free_strings( (char **)strings, __VA_ARGS__ )
#endif #endif
bool __mpw_realloc(
const void **buffer, size_t *bufferSize, const size_t deltaSize);
bool __mpw_free(
void **buffer, size_t bufferSize);
bool __mpw_free_string(
char **string);
bool __mpw_free_strings(
char **strings, ...);
void mpw_zero(
void *buffer, size_t bufferSize);
//// Cryptographic functions. //// Cryptographic functions.

View File

@@ -66,6 +66,7 @@ public class MPMasterKey {
* *
* @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object. * @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object.
*/ */
@Nonnull
public byte[] getKeyID(final MPAlgorithm algorithm) public byte[] getKeyID(final MPAlgorithm algorithm)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@@ -87,6 +88,7 @@ public class MPMasterKey {
return !invalidated; return !invalidated;
} }
@Nonnull
private byte[] masterKey(final MPAlgorithm algorithm) private byte[] masterKey(final MPAlgorithm algorithm)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
Preconditions.checkArgument( masterPassword.length > 0 ); Preconditions.checkArgument( masterPassword.length > 0 );
@@ -109,6 +111,7 @@ public class MPMasterKey {
return masterKey; return masterKey;
} }
@Nonnull
private byte[] siteKey(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter, private byte[] siteKey(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter,
final MPKeyPurpose keyPurpose, @Nullable final String keyContext) final MPKeyPurpose keyPurpose, @Nullable final String keyContext)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@@ -141,13 +144,20 @@ public class MPMasterKey {
* In the case of {@link MPResultTypeClass#Stateful} types, the result of * In the case of {@link MPResultTypeClass#Stateful} types, the result of
* {@link #siteState(String, MPAlgorithm, UnsignedInteger, MPKeyPurpose, String, MPResultType, String)}. * {@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. * @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object.
* @throws MPAlgorithmException An internal system or algorithm error has occurred.
*/ */
@Nullable
public String siteResult(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter, public String siteResult(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter,
final MPKeyPurpose keyPurpose, @Nullable final String keyContext, final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
final MPResultType resultType, @Nullable final String resultParam) final MPResultType resultType, @Nullable final String resultParam)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
if ((resultType.getTypeClass() == MPResultTypeClass.Stateful) && (resultParam == null))
return null;
byte[] masterKey = masterKey( algorithm ); byte[] masterKey = masterKey( algorithm );
byte[] siteKey = siteKey( siteName, algorithm, siteCounter, keyPurpose, keyContext ); byte[] siteKey = siteKey( siteName, algorithm, siteCounter, keyPurpose, keyContext );
@@ -175,7 +185,9 @@ public class MPMasterKey {
* {@link #siteResult(String, MPAlgorithm, UnsignedInteger, MPKeyPurpose, String, MPResultType, String)}. * {@link #siteResult(String, MPAlgorithm, UnsignedInteger, MPKeyPurpose, String, MPResultType, String)}.
* *
* @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object. * @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object.
* @throws MPAlgorithmException An internal system or algorithm error has occurred.
*/ */
@Nonnull
public String siteState(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter, public String siteState(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter,
final MPKeyPurpose keyPurpose, @Nullable final String keyContext, final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
final MPResultType resultType, final String resultParam) final MPResultType resultType, final String resultParam)

View File

@@ -41,7 +41,7 @@ public enum MPResultType {
/** /**
* 16: pg^VMAUBk5x3p%HP%i4= * 16: pg^VMAUBk5x3p%HP%i4=
*/ */
GeneratedMaximum( "maximum", "Maximum Security", "pg^VMAUBk5x3p%HP%i4=", "20 characters, contains symbols.", // GeneratedMaximum( "maximum", "Maximum Security", "pg^VMAUBk5x3p%HP%i4=", "20 characters, contains symbols", //
ImmutableList.of( new MPTemplate( "anoxxxxxxxxxxxxxxxxx" ), ImmutableList.of( new MPTemplate( "anoxxxxxxxxxxxxxxxxx" ),
new MPTemplate( "axxxxxxxxxxxxxxxxxno" ) ), // new MPTemplate( "axxxxxxxxxxxxxxxxxno" ) ), //
MPResultTypeClass.Template, 0x0 ), MPResultTypeClass.Template, 0x0 ),
@@ -49,7 +49,7 @@ public enum MPResultType {
/** /**
* 17: BiroYena8:Kixa * 17: BiroYena8:Kixa
*/ */
GeneratedLong( "long", "Long Password", "BiroYena8:Kixa", "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" ), ImmutableList.of( new MPTemplate( "CvcvnoCvcvCvcv" ), new MPTemplate( "CvcvCvcvnoCvcv" ),
new MPTemplate( "CvcvCvcvCvcvno" ), new MPTemplate( "CvccnoCvcvCvcv" ), new MPTemplate( "CvcvCvcvCvcvno" ), new MPTemplate( "CvccnoCvcvCvcv" ),
new MPTemplate( "CvccCvcvnoCvcv" ), new MPTemplate( "CvccCvcvCvcvno" ), new MPTemplate( "CvccCvcvnoCvcv" ), new MPTemplate( "CvccCvcvCvcvno" ),
@@ -66,7 +66,7 @@ public enum MPResultType {
/** /**
* 18: BirSuj0- * 18: BirSuj0-
*/ */
GeneratedMedium( "medium", "Medium Password", "BirSuj0-", "Copy-friendly, 8 characters, contains symbols.", // GeneratedMedium( "medium", "Medium Password", "BirSuj0-", "Copy-friendly, 8 characters, contains symbols", //
ImmutableList.of( new MPTemplate( "CvcnoCvc" ), ImmutableList.of( new MPTemplate( "CvcnoCvc" ),
new MPTemplate( "CvcCvcno" ) ), // new MPTemplate( "CvcCvcno" ) ), //
MPResultTypeClass.Template, 0x2 ), MPResultTypeClass.Template, 0x2 ),
@@ -74,14 +74,14 @@ public enum MPResultType {
/** /**
* 19: Bir8 * 19: Bir8
*/ */
GeneratedShort( "short", "Short Password", "Bir8", "Copy-friendly, 4 characters, no symbols.", // GeneratedShort( "short", "Short Password", "Bir8", "Copy-friendly, 4 characters, no symbols", //
ImmutableList.of( new MPTemplate( "Cvcn" ) ), // ImmutableList.of( new MPTemplate( "Cvcn" ) ), //
MPResultTypeClass.Template, 0x3 ), MPResultTypeClass.Template, 0x3 ),
/** /**
* 20: pO98MoD0 * 20: pO98MoD0
*/ */
GeneratedBasic( "basic", "Basic Password", "pO98MoD0", "8 characters, no symbols.", // GeneratedBasic( "basic", "Basic Password", "pO98MoD0", "8 characters, no symbols", //
ImmutableList.of( new MPTemplate( "aaanaaan" ), ImmutableList.of( new MPTemplate( "aaanaaan" ),
new MPTemplate( "aannaaan" ), new MPTemplate( "aannaaan" ),
new MPTemplate( "aaannaaa" ) ), // new MPTemplate( "aaannaaa" ) ), //
@@ -90,44 +90,44 @@ public enum MPResultType {
/** /**
* 21: 2798 * 21: 2798
*/ */
GeneratedPIN( "pin", "PIN Code", "2798", "4 numbers.", // GeneratedPIN( "pin", "PIN Code", "2798", "4 numbers", //
ImmutableList.of( new MPTemplate( "nnnn" ) ), // ImmutableList.of( new MPTemplate( "nnnn" ) ), //
MPResultTypeClass.Template, 0x5 ), MPResultTypeClass.Template, 0x5 ),
/** /**
* 30: birsujano * 30: birsujano
*/ */
GeneratedName( "name", "Name", "birsujano", "9 letter name.", // GeneratedName( "name", "Name", "birsujano", "9 letter name", //
ImmutableList.of( new MPTemplate( "cvccvcvcv" ) ), // ImmutableList.of( new MPTemplate( "cvccvcvcv" ) ), //
MPResultTypeClass.Template, 0xE ), MPResultTypeClass.Template, 0xE ),
/** /**
* 31: bir yennoquce fefi * 31: bir yennoquce fefi
*/ */
GeneratedPhrase( "phrase", "Phrase", "bir yennoquce fefi", "20 character sentence.", // GeneratedPhrase( "phrase", "Phrase", "bir yennoquce fefi", "20 character sentence", //
ImmutableList.of( new MPTemplate( "cvcc cvc cvccvcv cvc" ), ImmutableList.of( new MPTemplate( "cvcc cvc cvccvcv cvc" ),
new MPTemplate( "cvc cvccvcvcv cvcv" ), new MPTemplate( "cvc cvccvcvcv cvcv" ),
new MPTemplate( "cv cvccv cvc cvcvccv" ) ), // new MPTemplate( "cv cvccv cvc cvcvccv" ) ), //
MPResultTypeClass.Template, 0xF ), MPResultTypeClass.Template, 0xF ),
/** /**
* 1056: Custom saved password. * 1056: Custom saved value.
*/ */
StoredPersonal( "personal", "Saved Password", null, "AES-encrypted, exportable.", // StoredPersonal( "personal", "Saved", null, "AES-encrypted, exportable", //
ImmutableList.<MPTemplate>of(), // ImmutableList.<MPTemplate>of(), //
MPResultTypeClass.Stateful, 0x0, MPSiteFeature.ExportContent ), 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", "Private Password", null, "AES-encrypted, not exported.", // StoredDevicePrivate( "device", "Private", null, "AES-encrypted, not exported", //
ImmutableList.<MPTemplate>of(), // ImmutableList.<MPTemplate>of(), //
MPResultTypeClass.Stateful, 0x1, MPSiteFeature.DevicePrivate ), MPResultTypeClass.Stateful, 0x1, MPSiteFeature.DevicePrivate ),
/** /**
* 4160: Derive a unique binary key. * 4160: Derive a unique binary key.
*/ */
DeriveKey( "key", "Binary Key", null, "Encryption key.", // DeriveKey( "key", "Binary Key", null, "Encryption key", //
ImmutableList.<MPTemplate>of(), // ImmutableList.<MPTemplate>of(), //
MPResultTypeClass.Derive, 0x0, MPSiteFeature.Alternative ); MPResultTypeClass.Derive, 0x0, MPSiteFeature.Alternative );

View File

@@ -18,14 +18,14 @@
package com.lyndir.masterpassword.impl; package com.lyndir.masterpassword.impl;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteStreams; import com.google.common.io.ByteStreams;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import java.io.*; import java.io.*;
import java.util.Locale; import java.util.*;
import java.util.function.Predicate;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -43,30 +43,35 @@ public final class Native {
private static final String NATIVES_PATH = "lib"; private static final String NATIVES_PATH = "lib";
@SuppressWarnings({ "HardcodedFileSeparator", "LoadLibraryWithNonConstantString" }) @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 to load the library using the native system.
try { try {
System.loadLibrary( name ); System.loadLibrary( name );
return; return true;
} }
catch (@SuppressWarnings("ErrorNotRethrown") final UnsatisfiedLinkError ignored) { catch (@SuppressWarnings("ErrorNotRethrown") final UnsatisfiedLinkError ignored) {
} }
// Try to find and open a stream to the packaged library resource. // Try to find and open a stream to the packaged library resource.
try {
String library = System.mapLibraryName( name ); String library = System.mapLibraryName( name );
int libraryDot = library.lastIndexOf( EXTENSION_SEPARATOR ); int libraryDot = library.lastIndexOf( EXTENSION_SEPARATOR );
String libraryName = (libraryDot > 0)? library.substring( 0, libraryDot ): library; String libraryName = (libraryDot > 0)? library.substring( 0, libraryDot ): library;
String libraryExtension = (libraryDot > 0)? library.substring( libraryDot ): ".lib"; String libraryExtension = (libraryDot > 0)? library.substring( libraryDot ): ".lib";
String libraryResource = getLibraryResource( library );
@Nullable
File libraryFile = null;
Set<String> libraryResources = getLibraryResources( library );
for (final String libraryResource : libraryResources) {
try {
InputStream libraryStream = context.getResourceAsStream( libraryResource ); InputStream libraryStream = context.getResourceAsStream( libraryResource );
if (libraryStream == null) if (libraryStream == null) {
throw new IllegalStateException( logger.dbg( "No resource for library: %s", libraryResource );
"Library: " + name + " (" + libraryResource + "), not found in class loader for: " + context ); continue;
}
// Write the library resource to a temporary file. // Write the library resource to a temporary file.
File libraryFile = File.createTempFile( libraryName, libraryExtension ); libraryFile = File.createTempFile( libraryName, libraryExtension );
FileOutputStream libraryFileStream = new FileOutputStream( libraryFile ); FileOutputStream libraryFileStream = new FileOutputStream( libraryFile );
try { try {
libraryFile.deleteOnExit(); libraryFile.deleteOnExit();
@@ -79,35 +84,120 @@ public final class Native {
// Load the library from the temporary file. // Load the library from the temporary file.
System.load( libraryFile.getAbsolutePath() ); System.load( libraryFile.getAbsolutePath() );
return true;
} }
catch (final IOException e) { catch (@SuppressWarnings("ErrorNotRethrown") final IOException | UnsatisfiedLinkError e) {
throw new IllegalStateException( "Couldn't extract library: " + name, 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 @Nonnull
private static String getLibraryResource(final String library) { private static Set<String> getLibraryResources(final String library) {
String system = ifNotNullElse( System.getProperty( "os.name" ), "linux" ).toLowerCase( Locale.ROOT );
String architecture = ifNotNullElse( System.getProperty( "os.arch" ), "x86" ).toLowerCase( Locale.ROOT );
// Standardize system naming in accordance with masterpassword-core. // Standardize system naming in accordance with masterpassword-core.
if (system.contains( "windows" )) Sys system = Sys.findCurrent();
system = "windows";
else if (system.contains( "mac os x" ) || system.contains( "darwin" ) || system.contains( "osx" ))
system = "macos";
else
system = "linux";
// Standardize architecture naming in accordance with masterpassword-core. // Standardize architecture naming in accordance with masterpassword-core.
if (ImmutableList.of( "arm", "arm-v7", "armv7", "arm32" ).contains( architecture )) Collection<Arch> architectures = new LinkedHashSet<>();
architecture = "arm"; architectures.add( Arch.findCurrent() );
else if (architecture.startsWith( "arm" )) architectures.addAll( Arrays.asList( Arch.values() ) );
architecture = "arm64";
else if (ImmutableList.of( "x86_64", "amd64", "x64", "x86-64" ).contains( architecture ))
architecture = "x86_64";
else
architecture = "x86";
return Joiner.on( RESOURCE_SEPARATOR ).join( "", NATIVES_PATH, system, architecture, library ); 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;
}
} }
} }

View File

@@ -19,6 +19,21 @@ public final class Utilities {
return consumer.apply( value ); 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;
}
public static String ifNotNullOrEmptyElse(@Nullable final String value, @Nonnull final String emptyValue) {
if ((value == null) || value.isEmpty())
return emptyValue;
return value;
}
@Nonnull @Nonnull
public static <T, R> R ifNotNullElse(@Nullable final T value, final Function<T, R> consumer, @Nonnull final R nullValue) { public static <T, R> R ifNotNullElse(@Nullable final T value, final Function<T, R> consumer, @Nonnull final R nullValue) {
if (value == null) if (value == null)

View File

@@ -11,6 +11,7 @@ dependencies {
implementation group: 'com.lyndir.lhunath.opal', name: 'opal-system', version: '1.7-p2' 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: 'ch.qos.logback', name: 'logback-classic', version: '1.1.2'
implementation group: 'com.yuvimasory', name: 'orange-extensions', version: '1.3.0' 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' ) compile project( ':masterpassword-model' )
} }
@@ -30,7 +31,7 @@ shadowJar {
storepass: System.getenv( 'STORE_PW' ), storepass: System.getenv( 'STORE_PW' ),
keypass: System.getenv( 'KEY_PW_DESKTOP' ), keypass: System.getenv( 'KEY_PW_DESKTOP' ),
preservelastmodified: 'true', preservelastmodified: 'true',
destdir: '.' ) signedJar: "${rootDir}/../public/site/${project.name}-${project.version}.jar" )
} }
} }

View File

@@ -1,26 +0,0 @@
package com.lyndir.masterpassword.gui;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.gui.util.Platform;
import com.lyndir.masterpassword.gui.util.Res;
import com.lyndir.masterpassword.gui.view.MasterPasswordFrame;
/**
* @author lhunath, 2018-07-28
*/
public class GUI {
private static final Logger logger = Logger.get( GUI.class );
private final MasterPasswordFrame frame = new MasterPasswordFrame();
public GUI() {
Platform.get().installAppForegroundHandler( this::open );
Platform.get().installAppReopenHandler( this::open );
}
public void open() {
Res.ui( () -> frame.setVisible( true ) );
}
}

View File

@@ -19,22 +19,40 @@
package com.lyndir.masterpassword.gui; package com.lyndir.masterpassword.gui;
import com.lyndir.lhunath.opal.system.util.ConversionUtils; import com.lyndir.lhunath.opal.system.util.ConversionUtils;
import com.lyndir.masterpassword.model.MPConstants; import com.lyndir.masterpassword.model.MPConfig;
import com.lyndir.masterpassword.model.MPModelConstants;
/** /**
* @author lhunath, 2014-08-31 * @author lhunath, 2014-08-31
*/ */
@SuppressWarnings("CallToSystemGetenv") @SuppressWarnings("CallToSystemGetenv")
public class Config { public class MPGuiConfig extends MPConfig {
private static final Config instance = new Config(); public static MPGuiConfig get() {
return get( MPGuiConfig.class );
public static Config get() {
return instance;
} }
Boolean checkForUpdates;
Boolean stayResident;
public boolean checkForUpdates() { public boolean checkForUpdates() {
return ConversionUtils.toBoolean( System.getenv( MPConstants.env_checkUpdates ) ).orElse( true ); return (checkForUpdates != null)? checkForUpdates:
ConversionUtils.toBoolean( System.getenv( MPModelConstants.env_checkUpdates ) ).orElse( true );
}
public void setCheckForUpdates(final boolean checkForUpdates) {
this.checkForUpdates = checkForUpdates;
MasterPassword.get().updateCheck();
setChanged();
}
public boolean stayResident() {
return (stayResident != null)? stayResident: false;
}
public void setStayResident(final boolean stayResident) {
this.stayResident = stayResident;
setChanged();
} }
} }

View File

@@ -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 );
}

View File

@@ -24,7 +24,11 @@ import com.google.common.base.Charsets;
import com.google.common.io.ByteSource; import com.google.common.io.ByteSource;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ObjectUtils; 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.lyndir.masterpassword.model.MPUser;
import com.tulskiy.keymaster.common.Provider;
import java.awt.*;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.*; import java.net.*;
@@ -43,11 +47,13 @@ public final class MasterPassword {
@SuppressWarnings("UnusedDeclaration") @SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MasterPassword.class ); private static final Logger logger = Logger.get( MasterPassword.class );
private static final MasterPassword instance = new MasterPassword(); private static final MasterPassword instance = new MasterPassword();
private final Provider keyMaster = Provider.getCurrentProvider( true );
private final Collection<Listener> listeners = new CopyOnWriteArraySet<>(); private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
@Nullable
private MasterPasswordFrame frame;
@Nullable @Nullable
private MPUser<?> activeUser; private MPUser<?> activeUser;
@@ -55,12 +61,13 @@ public final class MasterPassword {
return instance; return instance;
} }
public boolean addListener(final Listener listener) { public void addListener(final Listener listener) {
return listeners.add( listener ); if (listeners.add( listener ))
listener.onUserSelected( activeUser );
} }
public boolean removeListener(final Listener listener) { public void removeListener(final Listener listener) {
return listeners.remove( listener ); listeners.remove( listener );
} }
public void activateUser(final MPUser<?> user) { public void activateUser(final MPUser<?> user) {
@@ -72,9 +79,49 @@ public final class MasterPassword {
listener.onUserSelected( activeUser ); listener.onUserSelected( activeUser );
} }
private static void checkUpdate() { @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 static void main(final String... args) {
//Thread.setDefaultUncaughtExceptionHandler(
// (t, e) -> logger.bug( e, "Uncaught: %s", e.getLocalizedMessage() ) );
// Set the system look & feel, if available.
try { try {
String implementationVersion = MasterPassword.class.getPackage().getImplementationVersion(); UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
}
catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
}
// Create and open the UI.
get().open();
// UI features.
get().updateResidence();
get().updateCheck();
}
public void updateCheck() {
if (!MPGuiConfig.get().checkForUpdates())
return;
try {
String implementationVersion = version();
String latestVersion = new ByteSource() { String latestVersion = new ByteSource() {
@Override @Override
public InputStream openStream() public InputStream openStream()
@@ -86,16 +133,14 @@ public final class MasterPassword {
} }
}.asCharSource( Charsets.UTF_8 ).readFirstLine(); }.asCharSource( Charsets.UTF_8 ).readFirstLine();
if ((implementationVersion != null) && (latestVersion != null) && if ((implementationVersion != null) && !implementationVersion.equalsIgnoreCase( latestVersion )) {
!implementationVersion.equalsIgnoreCase( latestVersion )) {
logger.inf( "Implementation: <%s>", implementationVersion ); logger.inf( "Implementation: <%s>", implementationVersion );
logger.inf( "Latest : <%s>", latestVersion ); logger.inf( "Latest : <%s>", latestVersion );
logger.wrn( "You are not running the current official version. Please update from:%n%s", logger.wrn( "You are not running the current official version. Please update from:%n%s",
"https://masterpassword.app/masterpassword-gui.jar" ); "https://masterpassword.app/masterpassword-gui.jar" );
JOptionPane.showMessageDialog( null, JOptionPane.showMessageDialog( null, Components.linkLabel( strf(
strf( "A new version of Master Password is available.%n " "A new version of Master Password is available."
+ "Please download the latest version from %s", + "<p>Please download the latest version from <a href='https://masterpassword.app'>https://masterpassword.app</a>." ) ),
"https://masterpassword.app" ),
"Update Available", JOptionPane.INFORMATION_MESSAGE ); "Update Available", JOptionPane.INFORMATION_MESSAGE );
} }
} }
@@ -104,26 +149,15 @@ public final class MasterPassword {
} }
} }
public static void main(final String... args) { public void updateResidence() {
// Thread.setDefaultUncaughtExceptionHandler( Platform.get().installAppForegroundHandler( get()::open );
// (t, e) -> logger.bug( e, "Uncaught: %s", e.getLocalizedMessage() ) ); Platform.get().installAppReopenHandler( get()::open );
keyMaster.register( MPGuiConstants.ui_hotkey, hotKey -> get().open() );
// Try and set the system look & feel, if available.
try {
UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
}
catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
}
// Check online to see if this version has been superseded.
if (Config.get().checkForUpdates())
checkUpdate();
// Create a platform-specific GUI and open it.
new GUI().open();
} }
@SuppressWarnings("InterfaceMayBeAnnotatedFunctional")
public interface Listener { public interface Listener {
void onUserSelected(@Nullable MPUser<?> user); void onUserSelected(@Nullable MPUser<?> user);
} }
} }

View File

@@ -31,17 +31,7 @@ import javax.annotation.Nullable;
*/ */
public class MPIncognitoQuestion extends MPBasicQuestion { public class MPIncognitoQuestion extends MPBasicQuestion {
private final MPIncognitoSite site;
public MPIncognitoQuestion(final MPIncognitoSite site, final String keyword, @Nullable final MPResultType type) { public MPIncognitoQuestion(final MPIncognitoSite site, final String keyword, @Nullable final MPResultType type) {
super( keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) ); super( site, keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) );
this.site = site;
}
@Nonnull
@Override
public MPIncognitoSite getSite() {
return site;
} }
} }

View File

@@ -40,4 +40,10 @@ public class MPIncognitoSite extends MPBasicSite<MPIncognitoUser, MPIncognitoQue
@Nullable final MPResultType resultType, @Nullable final MPResultType loginType) { @Nullable final MPResultType resultType, @Nullable final MPResultType loginType) {
super( user, siteName, (algorithm == null)? user.getAlgorithm(): algorithm, counter, resultType, loginType ); super( user, siteName, (algorithm == null)? user.getAlgorithm(): algorithm, counter, resultType, loginType );
} }
@Nonnull
@Override
public MPIncognitoQuestion addQuestion(final String keyword) {
return addQuestion( new MPIncognitoQuestion( this, keyword, null ) );
}
} }

View File

@@ -20,6 +20,7 @@ package com.lyndir.masterpassword.gui.model;
import com.lyndir.masterpassword.MPAlgorithm; import com.lyndir.masterpassword.MPAlgorithm;
import com.lyndir.masterpassword.model.impl.MPBasicUser; import com.lyndir.masterpassword.model.impl.MPBasicUser;
import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -38,6 +39,7 @@ public class MPIncognitoUser extends MPBasicUser<MPIncognitoSite> {
return null; return null;
} }
@Nonnull
@Override @Override
public MPIncognitoSite addSite(final String siteName) { public MPIncognitoSite addSite(final String siteName) {
return addSite( new MPIncognitoSite( this, siteName ) ); return addSite( new MPIncognitoSite( this, siteName ) );

View File

@@ -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() );
}
}

View File

@@ -1,7 +1,8 @@
package com.lyndir.masterpassword.gui.model; package com.lyndir.masterpassword.gui.model;
import com.lyndir.masterpassword.model.*; import com.lyndir.masterpassword.model.*;
import com.lyndir.masterpassword.model.impl.*; import com.lyndir.masterpassword.model.impl.MPBasicSite;
import javax.annotation.Nonnull;
/** /**
@@ -12,4 +13,20 @@ public class MPNewSite extends MPBasicSite<MPUser<?>, MPQuestion> {
public MPNewSite(final MPUser<?> user, final String siteName) { public MPNewSite(final MPUser<?> user, final String siteName) {
super( user, 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." );
}
public <S extends MPSite<?>> S addTo(final MPUser<S> user) {
S site = user.addSite( getSiteName() );
site.setAlgorithm( getAlgorithm() );
site.setCounter( getCounter() );
site.setLoginType( getLoginType() );
site.setResultType( getResultType() );
return site;
}
} }

View File

@@ -1,6 +1,11 @@
package com.lyndir.masterpassword.gui.util; package com.lyndir.masterpassword.gui.util;
import static com.google.common.base.Preconditions.*;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.lyndir.lhunath.opal.system.logging.Logger;
import java.util.*; import java.util.*;
import java.util.function.Consumer; import java.util.function.Consumer;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -16,27 +21,25 @@ import javax.swing.event.ListSelectionListener;
public class CollectionListModel<E> extends AbstractListModel<E> public class CollectionListModel<E> extends AbstractListModel<E>
implements ComboBoxModel<E>, ListSelectionListener, Selectable<E, CollectionListModel<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<>(); private final List<E> model = new LinkedList<>();
@Nullable @Nullable
private E selectedItem;
private JList<E> list; private JList<E> list;
@Nullable @Nullable
private E selectedItem;
@Nullable
private Consumer<E> selectionConsumer; private Consumer<E> selectionConsumer;
@SafeVarargs @SafeVarargs
public static <E> CollectionListModel<E> copy(final E... elements) { public CollectionListModel(final E... elements) {
return copy( Arrays.asList( elements ) ); this( Arrays.asList( elements ) );
} }
public static <E> CollectionListModel<E> copy(final Collection<? extends E> elements) { public CollectionListModel(final Collection<? extends E> elements) {
CollectionListModel<E> model = new CollectionListModel<>(); model.addAll( elements );
synchronized (model) { selectedItem = getElementAt( 0 );
model.model.addAll( elements ); fireIntervalAdded( this, 0, model.size() );
model.selectedItem = model.getElementAt( 0 );
model.fireIntervalAdded( model, 0, model.model.size() );
return model;
}
} }
@Override @Override
@@ -44,8 +47,8 @@ public class CollectionListModel<E> extends AbstractListModel<E>
return model.size(); return model.size();
} }
@Override
@Nullable @Nullable
@Override
public synchronized E getElementAt(final int index) { public synchronized E getElementAt(final int index) {
return (index < model.size())? model.get( index ): null; return (index < model.size())? model.get( index ): null;
} }
@@ -56,16 +59,11 @@ public class CollectionListModel<E> extends AbstractListModel<E>
* This operation will mutate the internal model to reflect the given model. * This operation will mutate the internal model to reflect the given model.
* The given model will remain untouched and independent from this object. * The given model will remain untouched and independent from this object.
*/ */
@SuppressWarnings({ "unchecked", "SuspiciousToArrayCall" }) @SuppressWarnings({ "Guava", "AssignmentToForLoopParameter" })
public synchronized void set(final Collection<? extends E> elements) { public synchronized void set(final Iterable<? extends E> elements) {
set( (E[]) elements.toArray( new Object[0] ) );
}
@SuppressWarnings("AssignmentToForLoopParameter")
public synchronized void set(final E... elements) {
ListIterator<E> oldIt = model.listIterator(); ListIterator<E> oldIt = model.listIterator();
for (int from = 0; oldIt.hasNext(); ++from) { for (int from = 0; oldIt.hasNext(); ++from) {
int to = Arrays.binarySearch( elements, oldIt.next() ); int to = Iterables.indexOf( elements, Predicates.equalTo( oldIt.next() ) );
if (to != from) { if (to != from) {
oldIt.remove(); oldIt.remove();
@@ -74,24 +72,37 @@ public class CollectionListModel<E> extends AbstractListModel<E>
} }
} }
for (int to = 0; to < elements.length; ++to) { int to = 0;
E newSite = elements[to]; for (final E newSite : elements) {
if ((to >= model.size()) || !Objects.equals( model.get( to ), newSite )) { if ((to >= model.size()) || !Objects.equals( model.get( to ), newSite )) {
model.add( to, newSite ); model.add( to, newSite );
fireIntervalAdded( this, to, to ); fireIntervalAdded( this, to, to );
} }
++to;
} }
if ((selectedItem == null) || !model.contains( selectedItem )) if ((selectedItem == null) || !model.contains( selectedItem ))
setSelectedItem( getElementAt( 0 ) ); selectItem( getElementAt( 0 ) );
}
@SafeVarargs
public final synchronized void set(final E... elements) {
set( ImmutableList.copyOf( elements ) );
} }
@Override @Override
@SuppressWarnings({ "unchecked", "SuspiciousMethodCalls" }) @Deprecated
public synchronized void setSelectedItem(@Nullable final Object newSelectedItem) { @SuppressWarnings("unchecked")
if (!Objects.equals( selectedItem, newSelectedItem )) { public synchronized void setSelectedItem(@Nullable final Object/* E */ newSelectedItem) {
selectedItem = (E) newSelectedItem; selectItem( (E) newSelectedItem );
}
public synchronized CollectionListModel<E> selectItem(@Nullable final E newSelectedItem) {
if (Objects.equals( selectedItem, newSelectedItem ))
return this;
selectedItem = newSelectedItem;
fireContentsChanged( this, -1, -1 ); fireContentsChanged( this, -1, -1 );
//noinspection ObjectEquality //noinspection ObjectEquality
@@ -100,7 +111,7 @@ public class CollectionListModel<E> extends AbstractListModel<E>
if (selectionConsumer != null) if (selectionConsumer != null)
selectionConsumer.accept( selectedItem ); selectionConsumer.accept( selectedItem );
} return this;
} }
@Nullable @Nullable
@@ -131,7 +142,7 @@ public class CollectionListModel<E> extends AbstractListModel<E>
@Override @Override
public synchronized CollectionListModel<E> selection(@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) { public synchronized CollectionListModel<E> selection(@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
this.selectionConsumer = null; this.selectionConsumer = null;
setSelectedItem( selectedItem ); selectItem( selectedItem );
return selection( selectionConsumer ); return selection( selectionConsumer );
} }
@@ -139,7 +150,7 @@ public class CollectionListModel<E> extends AbstractListModel<E>
@Override @Override
public synchronized void valueChanged(final ListSelectionEvent event) { public synchronized void valueChanged(final ListSelectionEvent event) {
//noinspection ObjectEquality //noinspection ObjectEquality
if (!event.getValueIsAdjusting() && (event.getSource() == list) && (list.getModel() == this)) { if (!event.getValueIsAdjusting() && (event.getSource() == list) && (checkNotNull( list ).getModel() == this)) {
selectedItem = list.getSelectedValue(); selectedItem = list.getSelectedValue();
if (selectionConsumer != null) if (selectionConsumer != null)

View File

@@ -18,10 +18,13 @@
package com.lyndir.masterpassword.gui.util; package com.lyndir.masterpassword.gui.util;
import com.google.common.base.Strings;
import com.lyndir.lhunath.opal.system.logging.Logger;
import java.awt.*; import java.awt.*;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.io.File; import java.io.File;
import java.net.URISyntaxException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -30,9 +33,9 @@ import javax.annotation.Nullable;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.Border; import javax.swing.border.Border;
import javax.swing.border.CompoundBorder; import javax.swing.border.CompoundBorder;
import javax.swing.event.DocumentEvent; import javax.swing.event.HyperlinkEvent;
import javax.swing.event.DocumentListener; import javax.swing.text.*;
import javax.swing.text.DefaultFormatterFactory; import org.jetbrains.annotations.NonNls;
/** /**
@@ -41,8 +44,10 @@ import javax.swing.text.DefaultFormatterFactory;
@SuppressWarnings({ "SerializableStoresNonSerializable", "serial" }) @SuppressWarnings({ "SerializableStoresNonSerializable", "serial" })
public abstract class Components { public abstract class Components {
public static final float TEXT_SIZE_HEADING = 19f; private static final Logger logger = Logger.get( Components.class );
public static final float TEXT_SIZE_CONTROL = 13f;
public static final int TEXT_SIZE_HEADING = 19;
public static final int TEXT_SIZE_CONTROL = 13;
public static final int SIZE_MARGIN = 12; public static final int SIZE_MARGIN = 12;
public static final int SIZE_PADDING = 8; public static final int SIZE_PADDING = 8;
@@ -96,6 +101,7 @@ public abstract class Components {
public static int showDialog(@Nullable final Component owner, @Nullable final String title, final JOptionPane pane) { public static int showDialog(@Nullable final Component owner, @Nullable final String title, final JOptionPane pane) {
JDialog dialog = pane.createDialog( owner, title ); JDialog dialog = pane.createDialog( owner, title );
dialog.setMinimumSize( new Dimension( 520, 0 ) );
dialog.setModalityType( Dialog.ModalityType.DOCUMENT_MODAL ); dialog.setModalityType( Dialog.ModalityType.DOCUMENT_MODAL );
showDialog( dialog ); showDialog( dialog );
@@ -107,9 +113,14 @@ public abstract class Components {
if (options == null) if (options == null)
return (selectedValue instanceof Integer)? (Integer) selectedValue: JOptionPane.CLOSED_OPTION; return (selectedValue instanceof Integer)? (Integer) selectedValue: JOptionPane.CLOSED_OPTION;
try {
int option = Arrays.binarySearch( options, selectedValue ); int option = Arrays.binarySearch( options, selectedValue );
return (option < 0)? JOptionPane.CLOSED_OPTION: option; return (option < 0)? JOptionPane.CLOSED_OPTION: option;
} }
catch (final ClassCastException ignored) {
return JOptionPane.CLOSED_OPTION;
}
}
@Nullable @Nullable
public static File showLoadDialog(@Nullable final Component owner, final String title) { public static File showLoadDialog(@Nullable final Component owner, final String title) {
@@ -139,7 +150,6 @@ public abstract class Components {
title, Dialog.ModalityType.DOCUMENT_MODAL ); title, Dialog.ModalityType.DOCUMENT_MODAL );
dialog.setMinimumSize( new Dimension( 320, 0 ) ); dialog.setMinimumSize( new Dimension( 320, 0 ) );
dialog.setLocationRelativeTo( owner ); dialog.setLocationRelativeTo( owner );
dialog.setLocationByPlatform( true );
dialog.setContentPane( content ); dialog.setContentPane( content );
return showDialog( dialog ); return showDialog( dialog );
@@ -151,13 +161,18 @@ public abstract class Components {
dialog.getRootPane().putClientProperty( "Window.style", "small" ); dialog.getRootPane().putClientProperty( "Window.style", "small" );
dialog.pack(); dialog.pack();
dialog.setLocationByPlatform( true );
dialog.setVisible( true ); dialog.setVisible( true );
return dialog; return dialog;
} }
public static JTextField textField() { 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 ), setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) ); BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) );
@@ -167,41 +182,30 @@ public abstract class Components {
@Override @Override
public Dimension getMaximumSize() { 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> selection) { public static JTextField textField(@Nullable final String text, @Nullable final Consumer<String> change) {
return new JTextField( text ) { 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 ), setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) ); BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) );
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) ); setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
setAlignmentX( LEFT_ALIGNMENT ); setAlignmentX( LEFT_ALIGNMENT );
if (selection != null) setLineWrap( true );
getDocument().addDocumentListener( new DocumentListener() { setRows( 3 );
@Override
public void insertUpdate(final DocumentEvent e) {
selection.accept( getText() );
}
@Override
public void removeUpdate(final DocumentEvent e) {
selection.accept( getText() );
}
@Override
public void changedUpdate(final DocumentEvent e) {
selection.accept( getText() );
}
} );
} }
@Override @Override
public Dimension getMaximumSize() { public Dimension getMaximumSize() {
return new Dimension( Integer.MAX_VALUE, getPreferredSize().height ); return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE );
} }
}; };
} }
@@ -224,20 +228,27 @@ public abstract class Components {
public static <E> JList<E> list(final ListModel<E> model, final Function<E, String> valueTransformer) { public static <E> JList<E> list(final ListModel<E> model, final Function<E, String> valueTransformer) {
return new JList<E>( model ) { return new JList<E>( model ) {
{ {
setAlignmentX( LEFT_ALIGNMENT );
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) ); setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
setCellRenderer( new DefaultListCellRenderer() { setCellRenderer( new DefaultListCellRenderer() {
@Override @Override
@SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" }) @SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" })
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index, public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
final boolean isSelected, final boolean cellHasFocus) { final boolean isSelected, final boolean cellHasFocus) {
String label = valueTransformer.apply( (E) value );
super.getListCellRendererComponent( super.getListCellRendererComponent(
list, valueTransformer.apply( (E) value ), index, isSelected, cellHasFocus ); list, Strings.isNullOrEmpty( label )? " ": label, index, isSelected, cellHasFocus );
setBorder( BorderFactory.createEmptyBorder( 2, 4, 2, 4 ) ); setBorder( BorderFactory.createEmptyBorder( 2, 4, 2, 4 ) );
return this; return this;
} }
} ); } );
setAlignmentX( LEFT_ALIGNMENT ); Dimension cellSize = getCellRenderer().getListCellRendererComponent( this, null, 0, false, false ).getPreferredSize();
setFixedCellWidth( cellSize.width );
setFixedCellHeight( cellSize.height );
if (model instanceof CollectionListModel)
((CollectionListModel<E>) model).registerList( this );
} }
@Override @Override
@@ -293,7 +304,6 @@ public abstract class Components {
public static JButton button(final Action action) { public static JButton button(final Action action) {
return new JButton( action ) { return new JButton( action ) {
{ {
setFont( Res.fonts().controlFont( TEXT_SIZE_CONTROL ) );
setAlignmentX( LEFT_ALIGNMENT ); setAlignmentX( LEFT_ALIGNMENT );
if (getText() == null) { if (getText() == null) {
@@ -370,7 +380,7 @@ public abstract class Components {
public static JLabel heading(@Nullable final String heading, final int horizontalAlignment) { public static JLabel heading(@Nullable final String heading, final int horizontalAlignment) {
return new JLabel( heading, horizontalAlignment ) { return new JLabel( heading, horizontalAlignment ) {
{ {
setFont( Res.fonts().controlFont( TEXT_SIZE_HEADING ).deriveFont( Font.BOLD ) ); setFont( getFont().deriveFont( Font.BOLD, TEXT_SIZE_HEADING ) );
setAlignmentX( LEFT_ALIGNMENT ); setAlignmentX( LEFT_ALIGNMENT );
} }
@@ -405,7 +415,6 @@ public abstract class Components {
public static JLabel label(@Nullable final String label, final int horizontalAlignment) { public static JLabel label(@Nullable final String label, final int horizontalAlignment) {
return new JLabel( label, horizontalAlignment ) { return new JLabel( label, horizontalAlignment ) {
{ {
setFont( Res.fonts().controlFont( TEXT_SIZE_CONTROL ) );
setAlignmentX( LEFT_ALIGNMENT ); setAlignmentX( LEFT_ALIGNMENT );
} }
@@ -417,11 +426,18 @@ public abstract class Components {
} }
public static JCheckBox checkBox(final String label) { public static JCheckBox checkBox(final String label) {
return checkBox( label, false, null );
}
public static JCheckBox checkBox(final String label, final boolean selected, @Nullable final Consumer<Boolean> selectionConsumer) {
return new JCheckBox( label ) { return new JCheckBox( label ) {
{ {
setFont( Res.fonts().controlFont( TEXT_SIZE_CONTROL ) );
setBackground( null ); setBackground( null );
setAlignmentX( LEFT_ALIGNMENT ); setAlignmentX( LEFT_ALIGNMENT );
setSelected( selected );
if (selectionConsumer != null)
addItemListener( e -> selectionConsumer.accept( isSelected() ) );
} }
}; };
} }
@@ -433,17 +449,17 @@ public abstract class Components {
public static <E> JComboBox<E> comboBox(final E[] values, final Function<E, String> valueTransformer, public static <E> JComboBox<E> comboBox(final E[] values, final Function<E, String> valueTransformer,
@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) { @Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
return comboBox( CollectionListModel.copy( values ).selection( selectedItem, selectionConsumer ), valueTransformer ); return comboBox( new CollectionListModel<>( values ).selection( selectedItem, selectionConsumer ), valueTransformer );
} }
public static <E> JComboBox<E> comboBox(final Collection<E> values, final Function<E, String> valueTransformer, public static <E> JComboBox<E> comboBox(final Collection<E> values, final Function<E, String> valueTransformer,
@Nullable final Consumer<E> selectionConsumer) { @Nullable final Consumer<E> selectionConsumer) {
return comboBox( CollectionListModel.copy( values ).selection( selectionConsumer ), valueTransformer ); return comboBox( new CollectionListModel<>( values ).selection( selectionConsumer ), valueTransformer );
} }
public static <E> JComboBox<E> comboBox(final Collection<E> values, final Function<E, String> 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) { @Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
return comboBox( CollectionListModel.copy( values ).selection( selectedItem, selectionConsumer ), valueTransformer ); return comboBox( new CollectionListModel<>( values ).selection( selectedItem, selectionConsumer ), valueTransformer );
} }
public static <E> JComboBox<E> comboBox(final ComboBoxModel<E> model, final Function<E, String> valueTransformer) { public static <E> JComboBox<E> comboBox(final ComboBoxModel<E> model, final Function<E, String> valueTransformer) {
@@ -456,8 +472,9 @@ public abstract class Components {
@SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" }) @SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" })
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index, public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
final boolean isSelected, final boolean cellHasFocus) { final boolean isSelected, final boolean cellHasFocus) {
String label = valueTransformer.apply( (E) value );
super.getListCellRendererComponent( super.getListCellRendererComponent(
list, valueTransformer.apply( (E) value ), index, isSelected, cellHasFocus ); list, Strings.isNullOrEmpty( label )? " ": label, index, isSelected, cellHasFocus );
setBorder( BorderFactory.createEmptyBorder( 0, 4, 0, 4 ) ); setBorder( BorderFactory.createEmptyBorder( 0, 4, 0, 4 ) );
return this; return this;
@@ -474,6 +491,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 { public static class GradientPanel extends JPanel {
@Nullable @Nullable

View File

@@ -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();
}
}

View File

@@ -0,0 +1,98 @@
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) {
setText( selectedItem );
selection( selectionConsumer );
if (selectionConsumer != null)
selectionConsumer.accept( selectedItem );
return this;
}
}

View File

@@ -20,17 +20,15 @@ package com.lyndir.masterpassword.gui.util;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*; 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.io.Resources;
import com.google.common.util.concurrent.*; import com.google.common.util.concurrent.*;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.MPIdenticon; import com.lyndir.masterpassword.MPIdenticon;
import com.lyndir.masterpassword.gui.SwingExecutorService;
import java.awt.*; import java.awt.*;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.Map;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import javax.swing.*; import javax.swing.*;
import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NonNls;
import org.joda.time.*; import org.joda.time.*;
@@ -80,6 +78,15 @@ public abstract class Res {
ui( true, job ); ui( true, job );
} }
public static <V> void ui(final ListenableFuture<V> future, final Consumer<V> job) {
Futures.addCallback( future, new FailableCallback<V>( logger ) {
@Override
public void onSuccess(@Nullable final V result) {
job.accept( result );
}
}, uiExecutor() );
}
public static void ui(final boolean immediate, final Runnable job) { public static void ui(final boolean immediate, final Runnable job) {
uiExecutor( immediate ).execute( job ); uiExecutor( immediate ).execute( job );
} }
@@ -138,6 +145,10 @@ public abstract class Res {
return icon( "media/icon_import.png" ); return icon( "media/icon_import.png" );
} }
public Icon help() {
return icon( "media/icon_help.png" );
}
public Icon export() { public Icon export() {
return icon( "media/icon_export.png" ); return icon( "media/icon_export.png" );
} }
@@ -146,6 +157,14 @@ public abstract class Res {
return icon( "media/icon_settings.png" ); 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) { public Icon avatar(final int index) {
return icon( strf( "media/avatar-%d.png", index % avatars() ) ); return icon( strf( "media/avatar-%d.png", index % avatars() ) );
} }
@@ -162,88 +181,67 @@ public abstract class Res {
public static final class Fonts { public static final class Fonts {
public Font emoticonsFont(final float size) { public Font emoticonsFont(final int size) {
return emoticonsRegular().deriveFont( size ); return MPFont.emoticonsRegular.get( size );
} }
public Font controlFont(final float size) { public Font controlFont(final int size) {
return exoRegular().deriveFont( size ); return MPFont.exoRegular.get( size );
} }
public Font valueFont(final float size) { public Font valueFont(final int size) {
return sourceSansProRegular().deriveFont( size ); return MPFont.sourceSansProRegular.get( size );
} }
public Font bigValueFont(final float size) { public Font bigValueFont(final int size) {
return sourceSansProBlack().deriveFont( size ); return MPFont.sourceSansProBlack.get( size );
} }
public Font emoticonsRegular() { private enum MPFont {
return font( "fonts/Emoticons-Regular.otf" ); emoticonsRegular( "Emoticons", "fonts/Emoticons-Regular.otf" ),
sourceCodeProRegular( "Source Code Pro", "fonts/SourceCodePro-Regular.otf" ),
sourceCodeProBlack( "Source Code Pro Bold", "fonts/SourceCodePro-Bold.otf" ),
sourceSansProRegular( "Source Sans Pro", "fonts/SourceSansPro-Regular.otf" ),
sourceSansProBlack( "Source Sans Pro Bold", "fonts/SourceSansPro-Bold.otf" ),
exoBold( "Exo 2.0 Bold", "fonts/Exo2.0-Bold.otf" ),
exoExtraBold( "Exo 2.0 Extra Bold", "fonts/Exo2.0-ExtraBold.otf" ),
exoRegular( "Exo 2.0", "fonts/Exo2.0-Regular.otf" ),
exoThin( "Exo 2.0 Thin", "fonts/Exo2.0-Thin.otf" ),
arimoBold( "Arimo Bold", "fonts/Arimo-Bold.ttf" ),
arimoBoldItalic( "Arimo Bold Italic", "fonts/Arimo-BoldItalic.ttf" ),
arimoItalic( "Arimo Italic", "fonts/Arimo-Italic.ttf" ),
arimoRegular( "Arimo", "fonts/Arimo-Regular.ttf" );
private final String fontName;
private final String resourceName;
private boolean registered;
MPFont(final String fontName, final String resourceName) {
this.fontName = fontName;
this.resourceName = resourceName;
} }
public Font sourceCodeProRegular() { Font get(final int size) {
return font( "fonts/SourceCodePro-Regular.otf" ); return get( Font.PLAIN, size );
} }
public Font sourceCodeProBlack() { Font get(final int style, final int size) {
return font( "fonts/SourceCodePro-Bold.otf" ); if (!registered)
register();
return new Font( fontName, style, size );
} }
public Font sourceSansProRegular() { private void register() {
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 { try {
fontsByResourceName.put( fontResourceName, new SoftReference<>( Font font = Font.createFont( Font.TRUETYPE_FONT, Resources.getResource( resourceName ).openStream() );
font = Font.createFont( Font.TRUETYPE_FONT, Resources.getResource( fontResourceName ).openStream() ) ) ); GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont( font );
registered = true;
} }
catch (final FontFormatException | IOException e) { catch (final FontFormatException | IOException e) {
throw logger.bug( e ); throw logger.bug( e );
} }
}
return font;
} }
} }

View File

@@ -11,5 +11,5 @@ public interface Selectable<E, T> {
T selection(@Nullable Consumer<E> selectionConsumer); T selection(@Nullable Consumer<E> selectionConsumer);
T selection(E selectedItem, @Nullable Consumer<E> selectionConsumer); T selection(@Nullable E selectedItem, @Nullable Consumer<E> selectionConsumer);
} }

View File

@@ -1,4 +1,4 @@
package com.lyndir.masterpassword.gui; package com.lyndir.masterpassword.gui.util;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*; import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
@@ -29,9 +29,9 @@ public class SwingExecutorService extends AbstractExecutorService {
@Override @Override
public void shutdown() { public void shutdown() {
synchronized (pendingCommands) {
shutdown = true; shutdown = true;
synchronized (pendingCommands) {
if (pendingCommands.isEmpty()) if (pendingCommands.isEmpty())
terminated.add( true ); terminated.add( true );
} }
@@ -49,8 +49,10 @@ public class SwingExecutorService extends AbstractExecutorService {
@Override @Override
public boolean isShutdown() { public boolean isShutdown() {
synchronized (pendingCommands) {
return shutdown; return shutdown;
} }
}
@Override @Override
public boolean isTerminated() { public boolean isTerminated() {
@@ -65,10 +67,10 @@ public class SwingExecutorService extends AbstractExecutorService {
@Override @Override
public void execute(@NotNull final Runnable command) { public void execute(@NotNull final Runnable command) {
synchronized (pendingCommands) {
if (shutdown) if (shutdown)
throw new RejectedExecutionException( "Executor is shut down." ); throw new RejectedExecutionException( "Executor is shut down." );
synchronized (pendingCommands) {
pendingCommands.add( command ); pendingCommands.add( command );
} }

View File

@@ -109,13 +109,14 @@ public class UnsignedIntegerModel extends SpinnerNumberModel implements Selectab
} }
@Override @Override
public UnsignedIntegerModel selection(final UnsignedInteger selectedItem, @Nullable final Consumer<UnsignedInteger> selectionConsumer) { public UnsignedIntegerModel selection(@Nullable final UnsignedInteger selectedItem,
@Nullable final Consumer<UnsignedInteger> selectionConsumer) {
if (changeListener != null) { if (changeListener != null) {
removeChangeListener( changeListener ); removeChangeListener( changeListener );
changeListener = null; changeListener = null;
} }
setValue( selectedItem ); setValue( (selectedItem != null)? selectedItem: getMinimum() );
return selection( selectionConsumer ); return selection( selectionConsumer );
} }

View File

@@ -3,9 +3,9 @@ package com.lyndir.masterpassword.gui.util.platform;
import com.apple.eawt.*; import com.apple.eawt.*;
import com.apple.eio.FileManager; import com.apple.eio.FileManager;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.base.Throwables; import com.lyndir.lhunath.opal.system.logging.Logger;
import java.io.File; import java.io.*;
import java.io.FileNotFoundException; import java.net.URI;
/** /**
@@ -13,12 +13,17 @@ import java.io.FileNotFoundException;
*/ */
public class ApplePlatform implements IPlatform { public class ApplePlatform implements IPlatform {
static Application application = Preconditions.checkNotNull( private static final Logger logger = Logger.get( ApplePlatform.class );
private static final Application application = Preconditions.checkNotNull(
Application.getApplication(), "Not an Apple Java application." ); Application.getApplication(), "Not an Apple Java application." );
private AppForegroundListener appForegroundHandler;
private AppReOpenedListener appReopenHandler;
@Override @Override
public boolean installAppForegroundHandler(final Runnable handler) { public boolean installAppForegroundHandler(final Runnable handler) {
application.addAppEventListener( new AppForegroundListener() { if (appForegroundHandler == null)
application.addAppEventListener( appForegroundHandler = new AppForegroundListener() {
@Override @Override
public void appMovedToBackground(final AppEvent.AppForegroundEvent e) { public void appMovedToBackground(final AppEvent.AppForegroundEvent e) {
} }
@@ -28,12 +33,37 @@ public class ApplePlatform implements IPlatform {
handler.run(); handler.run();
} }
} ); } );
return true;
}
@Override
public boolean removeAppForegroundHandler() {
if (appForegroundHandler == null)
return false;
application.removeAppEventListener( appForegroundHandler );
return true; return true;
} }
@Override @Override
public boolean installAppReopenHandler(final Runnable handler) { public boolean installAppReopenHandler(final Runnable handler) {
application.addAppEventListener( (AppReOpenedListener) e -> handler.run() ); application.addAppEventListener( appReopenHandler = e -> handler.run() );
return true;
}
@Override
public boolean removeAppReopenHandler() {
if (appReopenHandler == null)
return false;
application.removeAppEventListener( appReopenHandler );
return true;
}
@Override
public boolean requestForeground() {
application.requestForeground( true );
return true; return true;
} }
@@ -42,7 +72,20 @@ public class ApplePlatform implements IPlatform {
try { try {
return FileManager.revealInFinder( file ); return FileManager.revealInFinder( file );
} }
catch (final FileNotFoundException ignored) { 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; return false;
} }
} }

View File

@@ -1,6 +1,7 @@
package com.lyndir.masterpassword.gui.util.platform; package com.lyndir.masterpassword.gui.util.platform;
import java.io.File; import java.io.File;
import java.net.URI;
/** /**
@@ -13,13 +14,33 @@ public class BasePlatform implements IPlatform {
return false; return false;
} }
@Override
public boolean removeAppForegroundHandler() {
return false;
}
@Override @Override
public boolean installAppReopenHandler(final Runnable handler) { public boolean installAppReopenHandler(final Runnable handler) {
return false; return false;
} }
@Override
public boolean removeAppReopenHandler() {
return false;
}
@Override
public boolean requestForeground() {
return false;
}
@Override @Override
public boolean show(final File file) { public boolean show(final File file) {
return false; return false;
} }
@Override
public boolean open(final URI url) {
return false;
}
} }

View File

@@ -1,6 +1,8 @@
package com.lyndir.masterpassword.gui.util.platform; package com.lyndir.masterpassword.gui.util.platform;
import java.io.File; import java.io.File;
import java.net.URI;
import java.net.URL;
/** /**
@@ -10,7 +12,15 @@ public interface IPlatform {
boolean installAppForegroundHandler(Runnable handler); boolean installAppForegroundHandler(Runnable handler);
boolean removeAppForegroundHandler();
boolean installAppReopenHandler(Runnable handler); boolean installAppReopenHandler(Runnable handler);
boolean removeAppReopenHandler();
boolean requestForeground();
boolean show(File file); boolean show(File file);
boolean open(URI url);
} }

View File

@@ -1,8 +1,11 @@
package com.lyndir.masterpassword.gui.util.platform; package com.lyndir.masterpassword.gui.util.platform;
import com.lyndir.lhunath.opal.system.logging.Logger;
import java.awt.*; import java.awt.*;
import java.awt.desktop.*; import java.awt.desktop.*;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.net.URI;
/** /**
@@ -11,9 +14,16 @@ import java.io.File;
@SuppressWarnings("Since15") @SuppressWarnings("Since15")
public class JDK9Platform implements IPlatform { public class JDK9Platform implements IPlatform {
private static final Logger logger = Logger.get( JDK9Platform.class );
private static final Desktop desktop = Desktop.getDesktop();
private AppForegroundListener appForegroundHandler;
private AppReopenedListener appReopenHandler;
@Override @Override
public boolean installAppForegroundHandler(final Runnable handler) { public boolean installAppForegroundHandler(final Runnable handler) {
Desktop.getDesktop().addAppEventListener( new AppForegroundListener() { if (appForegroundHandler == null)
desktop.addAppEventListener( appForegroundHandler = new AppForegroundListener() {
@Override @Override
public void appRaisedToForeground(final AppForegroundEvent e) { public void appRaisedToForeground(final AppForegroundEvent e) {
handler.run(); handler.run();
@@ -23,12 +33,39 @@ public class JDK9Platform implements IPlatform {
public void appMovedToBackground(final AppForegroundEvent e) { public void appMovedToBackground(final AppForegroundEvent e) {
} }
} ); } );
return true;
}
@Override
public boolean removeAppForegroundHandler() {
if (appForegroundHandler == null)
return false;
desktop.removeAppEventListener( appForegroundHandler );
return true; return true;
} }
@Override @Override
public boolean installAppReopenHandler(final Runnable handler) { public boolean installAppReopenHandler(final Runnable handler) {
Desktop.getDesktop().addAppEventListener( (AppReopenedListener) e -> handler.run() ); if (appReopenHandler == null)
desktop.addAppEventListener( appReopenHandler = e -> handler.run() );
return true;
}
@Override
public boolean removeAppReopenHandler() {
if (appReopenHandler == null)
return false;
desktop.removeAppEventListener( appReopenHandler );
return true;
}
@Override
public boolean requestForeground() {
desktop.requestForeground( true );
return true; return true;
} }
@@ -37,7 +74,19 @@ public class JDK9Platform implements IPlatform {
if (!file.exists()) if (!file.exists())
return false; return false;
Desktop.getDesktop().browseFileDirectory( file ); desktop.browseFileDirectory( file );
return true; 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;
}
}
} }

View File

@@ -9,8 +9,6 @@ import com.lyndir.masterpassword.model.MPUser;
import com.lyndir.masterpassword.model.impl.MPFileUser; import com.lyndir.masterpassword.model.impl.MPFileUser;
import com.lyndir.masterpassword.model.impl.MPFileUserManager; import com.lyndir.masterpassword.model.impl.MPFileUserManager;
import java.awt.*; import java.awt.*;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.swing.*; import javax.swing.*;
@@ -25,9 +23,7 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener, Ma
"Click to change the user's avatar." ); "Click to change the user's avatar." );
private final CollectionListModel<MPUser<?>> usersModel = private final CollectionListModel<MPUser<?>> usersModel =
CollectionListModel.<MPUser<?>>copy( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser ); new CollectionListModel<MPUser<?>>( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser );
private final JComboBox<? extends MPUser<?>> userField =
Components.comboBox( usersModel, user -> ifNotNull( user, MPUser::getFullName ) );
protected FilesPanel() { protected FilesPanel() {
setOpaque( false ); setOpaque( false );
@@ -46,7 +42,7 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener, Ma
add( Components.strut( Components.margin() ) ); add( Components.strut( Components.margin() ) );
// User Selection // User Selection
add( userField ); add( Components.comboBox( usersModel, user -> ifNotNull( user, MPUser::getFullName ) ) );
MPFileUserManager.get().addListener( this ); MPFileUserManager.get().addListener( this );
MasterPassword.get().addListener( this ); MasterPassword.get().addListener( this );
@@ -68,7 +64,7 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener, Ma
@Override @Override
public void onUserSelected(@Nullable final MPUser<?> user) { public void onUserSelected(@Nullable final MPUser<?> user) {
usersModel.setSelectedItem( user ); usersModel.selectItem( user );
avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) ); avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) );
} }
} }

View File

@@ -1,14 +1,12 @@
package com.lyndir.masterpassword.gui.view; package com.lyndir.masterpassword.gui.view;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.gui.MPGuiConfig;
import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.gui.util.Components;
import com.lyndir.masterpassword.gui.util.Res; import com.lyndir.masterpassword.gui.util.Res;
import com.lyndir.masterpassword.model.MPUser;
import com.lyndir.masterpassword.model.impl.MPFileUserManager; import com.lyndir.masterpassword.model.impl.MPFileUserManager;
import java.awt.*; import java.awt.*;
import java.awt.event.ComponentAdapter; import java.awt.event.*;
import java.awt.event.ComponentEvent;
import javax.annotation.Nullable;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.BevelBorder; import javax.swing.border.BevelBorder;
@@ -21,28 +19,27 @@ public class MasterPasswordFrame extends JFrame {
private static final Logger logger = Logger.get( MasterPasswordFrame.class ); private static final Logger logger = Logger.get( MasterPasswordFrame.class );
@SuppressWarnings("FieldCanBeLocal") private final UserContentPanel userContent;
private final Components.GradientPanel root = Components.borderPanel( Res.colors().frameBg(), BoxLayout.PAGE_AXIS );
private final FilesPanel filesPanel = new FilesPanel();
private final JPanel userPanel = Components.panel( new BorderLayout( 0, 0 ) );
private final UserContentPanel userContent = new UserContentPanel();
@SuppressWarnings("MagicNumber") @SuppressWarnings("MagicNumber")
public MasterPasswordFrame() { public MasterPasswordFrame() {
super( "Master Password" ); super( "Master Password" );
setContentPane( root ); JPanel root, userPanel;
root.add( filesPanel ); setContentPane( root = Components.borderPanel( Res.colors().frameBg(), BoxLayout.PAGE_AXIS ) );
root.add( Components.strut() );
root.add( userPanel ); root.add( new FilesPanel() );
root.add( Components.strut() );
root.add( userPanel = Components.panel( new BorderLayout( 0, 0 ) ) );
userPanel.add( userContent.getUserToolbar(), BorderLayout.LINE_START );
userPanel.add( userContent.getSiteToolbar(), BorderLayout.LINE_END );
userPanel.add( Components.borderPanel( userPanel.add( Components.borderPanel(
BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ), BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ),
Res.colors().controlBg(), BoxLayout.PAGE_AXIS, userContent ), BorderLayout.CENTER ); 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() ); addComponentListener( new ComponentHandler() );
addWindowListener( new WindowHandler() );
setPreferredSize( new Dimension( 800, 560 ) ); setPreferredSize( new Dimension( 800, 560 ) );
setDefaultCloseOperation( DISPOSE_ON_CLOSE ); setDefaultCloseOperation( DISPOSE_ON_CLOSE );
pack(); pack();
@@ -59,4 +56,14 @@ public class MasterPasswordFrame extends JFrame {
userContent.transferFocus(); userContent.transferFocus();
} }
} }
private static class WindowHandler extends WindowAdapter {
@Override
public void windowClosed(final WindowEvent e) {
if (!MPGuiConfig.get().stayResident())
System.exit( 0 );
}
}
} }

View File

@@ -5,12 +5,12 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.base.*; import com.google.common.base.*;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.google.common.util.concurrent.ListenableFuture;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ObjectUtils; import com.lyndir.lhunath.opal.system.util.ObjectUtils;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.gui.MasterPassword; import com.lyndir.masterpassword.gui.*;
import com.lyndir.masterpassword.gui.model.MPIncognitoUser; import com.lyndir.masterpassword.gui.model.*;
import com.lyndir.masterpassword.gui.model.MPNewSite;
import com.lyndir.masterpassword.gui.util.*; import com.lyndir.masterpassword.gui.util.*;
import com.lyndir.masterpassword.gui.util.Platform; import com.lyndir.masterpassword.gui.util.Platform;
import com.lyndir.masterpassword.model.*; import com.lyndir.masterpassword.model.*;
@@ -27,12 +27,13 @@ import java.util.*;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.regex.Pattern;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.swing.*; import javax.swing.*;
import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener; import javax.swing.event.DocumentListener;
import javax.swing.text.PlainDocument;
/** /**
@@ -42,13 +43,18 @@ import javax.swing.event.DocumentListener;
public class UserContentPanel extends JPanel implements MasterPassword.Listener, MPUser.Listener { public class UserContentPanel extends JPanel implements MasterPassword.Listener, MPUser.Listener {
private static final Random random = new Random(); 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 Logger logger = Logger.get( UserContentPanel.class );
private static final JButton iconButton = Components.button( Res.icons().user(), null, null ); 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 static final Pattern EACH_CHARACTER = Pattern.compile( "." );
private final JButton addButton = Components.button( Res.icons().add(), event -> addUser(), private final JButton addButton = Components.button( Res.icons().add(), event -> addUser(),
"Add a new user to Master Password." ); "Add a new user to Master Password." );
private final JButton importButton = Components.button( Res.icons().import_(), event -> importUser(), private final JButton importButton = Components.button( Res.icons().import_(), event -> importUser(),
"Import a user from a backup file into Master Password." ); "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 userToolbar = Components.panel( BoxLayout.PAGE_AXIS );
private final JPanel siteToolbar = Components.panel( BoxLayout.PAGE_AXIS ); private final JPanel siteToolbar = Components.panel( BoxLayout.PAGE_AXIS );
@@ -208,7 +214,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
this, strf( "<html>Couldn't read import file:<br><pre>%s</pre></html>.", e.getLocalizedMessage() ), this, strf( "<html>Couldn't read import file:<br><pre>%s</pre></html>.", e.getLocalizedMessage() ),
"Import Failed", JOptionPane.ERROR_MESSAGE ); "Import Failed", JOptionPane.ERROR_MESSAGE );
} }
catch (MPMarshalException e) { catch (final MPMarshalException e) {
logger.err( e, "While parsing user import file." ); logger.err( e, "While parsing user import file." );
JOptionPane.showMessageDialog( JOptionPane.showMessageDialog(
this, strf( "<html>Couldn't parse import file:<br><pre>%s</pre></html>.", e.getLocalizedMessage() ), this, strf( "<html>Couldn't parse import file:<br><pre>%s</pre></html>.", e.getLocalizedMessage() ),
@@ -216,6 +222,32 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} }
} }
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 { private enum ContentMode {
NO_USER, NO_USER,
AUTHENTICATE, AUTHENTICATE,
@@ -239,6 +271,8 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
userToolbar.add( addButton ); userToolbar.add( addButton );
userToolbar.add( importButton ); userToolbar.add( importButton );
userToolbar.add( Box.createGlue() );
userToolbar.add( helpButton );
add( Box.createGlue() ); add( Box.createGlue() );
add( Components.heading( "Select a user to proceed." ) ); add( Components.heading( "Select a user to proceed." ) );
@@ -259,9 +293,9 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
private final JButton resetButton = Components.button( Res.icons().reset(), event -> resetUser(), private final JButton resetButton = Components.button( Res.icons().reset(), event -> resetUser(),
"Change the master password for this user." ); "Change the master password for this user." );
private final JPasswordField masterPasswordField = Components.passwordField(); private final JPasswordField masterPasswordField;
private final JLabel errorLabel = Components.label(); private final JLabel errorLabel;
private final JLabel identiconLabel = Components.label( SwingConstants.CENTER ); private final JLabel identiconLabel;
private Future<?> identiconJob; private Future<?> identiconJob;
@@ -275,20 +309,22 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
userToolbar.add( exportButton ); userToolbar.add( exportButton );
userToolbar.add( deleteButton ); userToolbar.add( deleteButton );
userToolbar.add( resetButton ); userToolbar.add( resetButton );
userToolbar.add( Box.createGlue() );
userToolbar.add( helpButton );
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) ); add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
add( Components.strut() ); add( Components.strut() );
add( identiconLabel ); add( identiconLabel = Components.label( SwingConstants.CENTER ) );
identiconLabel.setFont( Res.fonts().emoticonsFont( Components.TEXT_SIZE_CONTROL ) ); identiconLabel.setFont( Res.fonts().emoticonsFont( Components.TEXT_SIZE_CONTROL ) );
add( Box.createGlue() ); add( Box.createGlue() );
add( Components.label( "Master Password:" ) ); add( Components.label( "Master Password:" ) );
add( Components.strut() ); add( Components.strut() );
add( masterPasswordField ); add( masterPasswordField = Components.passwordField() );
masterPasswordField.addActionListener( this ); masterPasswordField.addActionListener( this );
masterPasswordField.getDocument().addDocumentListener( this ); masterPasswordField.getDocument().addDocumentListener( this );
add( errorLabel ); add( errorLabel = Components.label() );
errorLabel.setForeground( Res.colors().errorFg() ); errorLabel.setForeground( Res.colors().errorFg() );
add( Box.createGlue() ); add( Box.createGlue() );
} }
@@ -370,6 +406,9 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
Res.job( () -> { Res.job( () -> {
try { try {
user.authenticate( masterPassword ); user.authenticate( masterPassword );
if (user instanceof MPFileUser)
((MPFileUser) user).migrateTo( MPMarshalFormat.DEFAULT );
} }
catch (final MPIncorrectMasterPasswordException e) { catch (final MPIncorrectMasterPasswordException e) {
logger.wrn( e, "During user authentication for: %s", user ); logger.wrn( e, "During user authentication for: %s", user );
@@ -425,9 +464,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} }
private final class AuthenticatedUserPanel extends JPanel implements KeyListener, MPUser.Listener { private final class AuthenticatedUserPanel extends JPanel implements KeyListener, MPUser.Listener, KeyEventDispatcher {
public static final int SIZE_RESULT = 48;
private final JButton userButton = Components.button( Res.icons().user(), event -> showUserPreferences(), private final JButton userButton = Components.button( Res.icons().user(), event -> showUserPreferences(),
"Show user preferences." ); "Show user preferences." );
@@ -435,22 +472,28 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
"Sign out and lock user." ); "Sign out and lock user." );
private final JButton settingsButton = Components.button( Res.icons().settings(), event -> showSiteSettings(), private final JButton settingsButton = Components.button( Res.icons().settings(), event -> showSiteSettings(),
"Show site settings." ); "Show site settings." );
private final JButton questionsButton = Components.button( Res.icons().question(), null, private final JButton questionsButton = Components.button( Res.icons().question(), event -> showSiteQuestions(),
"Show site recovery questions." ); "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(), private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteSite(),
"Delete the site from the user." ); "Delete the site from the user." );
@Nonnull @Nonnull
private final MPUser<?> user; private final MPUser<?> user;
private final JLabel passwordLabel = Components.label( SwingConstants.CENTER ); private final JLabel resultLabel;
private final JLabel passwordField = Components.heading( SwingConstants.CENTER ); private final JLabel resultField;
private final JLabel queryLabel = Components.label(); private final JLabel answerLabel;
private final JTextField queryField = Components.textField( null, this::updateSites ); private final JLabel answerField;
private final CollectionListModel<MPSite<?>> sitesModel = private final JLabel queryLabel;
new CollectionListModel<MPSite<?>>().selection( this::showSiteResult ); private final JTextField queryField;
private final JList<MPSite<?>> sitesList = private final CollectionListModel<MPQuery.Result<? extends MPSite<?>>> sitesModel;
Components.list( sitesModel, this::getSiteDescription ); private final CollectionListModel<MPQuery.Result<? extends MPQuestion>> questionsModel;
private final JList<MPQuery.Result<? extends MPSite<?>>> sitesList;
private boolean showLogin;
private Future<?> updateSitesJob; private Future<?> updateSitesJob;
private AuthenticatedUserPanel(@Nonnull final MPUser<?> user) { private AuthenticatedUserPanel(@Nonnull final MPUser<?> user) {
@@ -461,56 +504,97 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
userToolbar.add( addButton ); userToolbar.add( addButton );
userToolbar.add( userButton ); userToolbar.add( userButton );
userToolbar.add( logoutButton ); userToolbar.add( logoutButton );
userToolbar.add( Box.createGlue() );
userToolbar.add( helpButton );
siteToolbar.add( settingsButton ); siteToolbar.add( settingsButton );
siteToolbar.add( questionsButton ); siteToolbar.add( questionsButton );
siteToolbar.add( editButton );
siteToolbar.add( keyButton );
siteToolbar.add( deleteButton ); siteToolbar.add( deleteButton );
settingsButton.setEnabled( false ); settingsButton.setEnabled( false );
questionsButton.setEnabled( false );
editButton.setEnabled( false );
keyButton.setEnabled( false );
deleteButton.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 ) );
questionsModel = new CollectionListModel<MPQuery.Result<? extends MPQuestion>>().selection( this::showQuestionItem );
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) ); add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
add( passwordLabel ); add( resultLabel = Components.label( SwingConstants.CENTER ) );
add( passwordField ); add( resultField = Components.heading( SwingConstants.CENTER ) );
passwordField.setForeground( Res.colors().highlightFg() ); resultField.setForeground( Res.colors().highlightFg() );
passwordField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) ); resultField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
add( Box.createGlue() ); add( Box.createGlue() );
add( Components.strut() ); add( Components.strut() );
add( queryLabel ); add( queryLabel = Components.label() );
queryLabel.setText( strf( "%s's password for:", user.getFullName() ) ); queryLabel.setText( strf( "%s's password for:", user.getFullName() ) );
add( queryField ); add( queryField = Components.textField( null, this::updateSites ) );
queryField.putClientProperty( "JTextField.variant", "search" ); queryField.putClientProperty( "JTextField.variant", "search" );
queryField.addActionListener( event -> useSite() ); queryField.addActionListener( this::useSite );
queryField.getInputMap().put( copyLoginKeyStroke, JTextField.notifyAction );
queryField.addKeyListener( this ); queryField.addKeyListener( this );
queryField.requestFocusInWindow(); queryField.requestFocusInWindow();
add( Components.strut() ); add( Components.strut() );
add( Components.scrollPane( sitesList ) );
sitesModel.registerList( sitesList ); add( Components.scrollPane( sitesList = Components.list(
add( Box.createGlue() ); sitesModel = new CollectionListModel<MPQuery.Result<? extends MPSite<?>>>().selection( this::showSiteItem ),
this::getSiteDescription ) ) );
sitesList.registerKeyboardAction( this::useSite, KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 ),
JComponent.WHEN_FOCUSED );
sitesList.registerKeyboardAction( this::useSite, KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK ),
JComponent.WHEN_FOCUSED );
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 -> { addHierarchyListener( e -> {
if (null != SwingUtilities.windowForComponent( this )) if (HierarchyEvent.DISPLAYABILITY_CHANGED == (e.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED)) {
if (null != SwingUtilities.windowForComponent( this )) {
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher( this );
user.addListener( this ); user.addListener( this );
else } else {
KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher( this );
user.removeListener( this ); user.removeListener( this );
}
}
} ); } );
} }
public void showUserPreferences() { public void showUserPreferences() {
ImmutableList.Builder<Component> components = ImmutableList.builder(); 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.add( Components.label( "Default Algorithm:" ),
Components.comboBox( MPAlgorithm.Version.values(), MPAlgorithm.Version::name, Components.comboBox( MPAlgorithm.Version.values(), MPAlgorithm.Version::name,
user.getAlgorithm().version(), user.getAlgorithm().version(), version -> user.setAlgorithm( version.getAlgorithm() ) ) );
version -> user.setAlgorithm( version.getAlgorithm() ) ) );
components.add( Components.label( "Default Password Type:" ),
Components.comboBox( MPResultType.values(), MPResultType::getLongName,
user.getPreferences().getDefaultType(), user.getPreferences()::setDefaultType ),
Components.strut() );
components.add( Components.checkBox( "Hide Passwords",
user.getPreferences().isHidePasswords(), user.getPreferences()::setHidePasswords ) );
components.add( new JSeparator() );
components.add( Components.checkBox( "Check For Updates",
MPGuiConfig.get().checkForUpdates(), MPGuiConfig.get()::setCheckForUpdates ) );
components.add( Components.checkBox( strf( "<html>Stay Resident (reactivate with <strong><code>%s+%s</code></strong>)",
InputEvent.getModifiersExText( MPGuiConstants.ui_hotkey.getModifiers() ),
KeyEvent.getKeyText( MPGuiConstants.ui_hotkey.getKeyCode() ) ),
MPGuiConfig.get().stayResident(), MPGuiConfig.get()::setStayResident ) );
Components.showDialog( this, user.getFullName(), new JOptionPane( Components.panel( Components.showDialog( this, user.getFullName(), new JOptionPane( Components.panel(
BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) ); BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) );
@@ -521,7 +605,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} }
public void showSiteSettings() { public void showSiteSettings() {
MPSite<?> site = sitesModel.getSelectedItem(); MPSite<?> site = getSite();
if (site == null) if (site == null)
return; return;
@@ -537,12 +621,14 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
Components.strut() ); Components.strut() );
components.add( Components.label( "Password Type:" ), components.add( Components.label( "Password Type:" ),
Components.comboBox( MPResultType.values(), MPResultType::getLongName, Components.comboBox( MPResultType.values(), type ->
getTypeDescription( type, user.getPreferences().getDefaultType() ),
site.getResultType(), site::setResultType ), site.getResultType(), site::setResultType ),
Components.strut() ); Components.strut() );
components.add( Components.label( "Login Type:" ), components.add( Components.label( "Login Type:" ),
Components.comboBox( MPResultType.values(), MPResultType::getLongName, Components.comboBox( MPResultType.values(), type ->
getTypeDescription( type, user.getAlgorithm().mpw_default_login_type() ),
site.getLoginType(), site::setLoginType ), site.getLoginType(), site::setLoginType ),
Components.strut() ); Components.strut() );
@@ -552,12 +638,183 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
Components.textField( fileSite.getUrl(), fileSite::setUrl ), Components.textField( fileSite.getUrl(), fileSite::setUrl ),
Components.strut() ); Components.strut() );
Components.showDialog( this, site.getSiteName(), new JOptionPane( Components.panel( Components.showDialog( this, strf( "Settings for %s", site.getSiteName() ), new JOptionPane( Components.panel(
BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) ); 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 = getSite();
if (site == null)
return;
JList<MPQuery.Result<? extends MPQuestion>> questionsList =
Components.list( questionsModel, this::getQuestionDescription );
JTextField queryField = Components.textField( null, queryText -> Res.job( () -> {
MPQuery query = new MPQuery( queryText );
Collection<MPQuery.Result<? extends MPQuestion>> questionItems = new LinkedList<MPQuery.Result<? extends MPQuestion>>(
query.find( site.getQuestions(), MPQuestion::getKeyword ) );
if (questionItems.stream().noneMatch( MPQuery.Result::isExact ))
questionItems.add( MPQuery.Result.allOf( new MPNewQuestion( site, query.getQuery() ), query.getQuery() ) );
Res.ui( () -> questionsModel.set( questionItems ) );
} ) );
queryField.putClientProperty( "JTextField.variant", "search" );
queryField.addActionListener( this::useQuestion );
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 = getSite();
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 = getSite();
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() { public void deleteSite() {
MPSite<?> site = sitesModel.getSelectedItem(); MPSite<?> site = getSite();
if (site == null) if (site == null)
return; return;
@@ -567,96 +824,193 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
user.deleteSite( site ); user.deleteSite( site );
} }
private String getSiteDescription(@Nonnull final MPSite<?> site) { private String getSiteDescription(@Nullable final MPQuery.Result<? extends MPSite<?>> item) {
MPSite<?> site = (item != null)? item.getValue(): null;
if (site == null)
return " ";
if (site instanceof MPNewSite) if (site instanceof MPNewSite)
return strf( "<html><strong>%s</strong> &lt;Add new site&gt;</html>", queryField.getText() ); return strf( "<html><strong>%s</strong> &lt;Add new site&gt;</html>", item.getKeyAsHTML() );
ImmutableList.Builder<Object> parameters = ImmutableList.builder(); ImmutableList.Builder<Object> parameters = ImmutableList.builder();
try {
MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null; MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null;
if (fileSite != null) if (fileSite != null)
parameters.add( Res.format( fileSite.getLastUsed() ) ); parameters.add( Res.format( fileSite.getLastUsed() ) );
parameters.add( site.getAlgorithm().version() ); parameters.add( site.getAlgorithm().version() );
parameters.add( strf( "#%d", site.getCounter().longValue() ) ); parameters.add( strf( "#%d", site.getCounter().longValue() ) );
parameters.add( strf( "<em>%s</em>", site.getLogin() ) );
if ((fileSite != null) && (fileSite.getUrl() != null)) if ((fileSite != null) && (fileSite.getUrl() != null))
parameters.add( fileSite.getUrl() ); 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(), return strf( "<html><strong>%s</strong> (%s)</html>", item.getKeyAsHTML(),
Joiner.on( " - " ).skipNulls().join( parameters.build() ) ); Joiner.on( " - " ).skipNulls().join( parameters.build() ) );
} }
private void useSite() { private String getQuestionDescription(@Nullable final MPQuery.Result<? extends MPQuestion> item) {
MPSite<?> site = sitesModel.getSelectedItem(); MPQuestion question = (item != null)? item.getValue(): null;
if (site instanceof MPNewSite) { if (question == null)
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog( return "<site>";
this, strf( "<html>Remember the site <strong>%s</strong>?</html>", site.getSiteName() ), if (question instanceof MPNewQuestion)
"New Site", JOptionPane.YES_NO_OPTION )) { return strf( "<html>%s &lt;Add new question&gt;</html>", item.getKeyAsHTML() );
sitesModel.setSelectedItem( user.addSite( site.getSiteName() ) );
useSite(); return strf( "<html>%s</html>", item.getKeyAsHTML() );
}
return;
} }
showSiteResult( site, result -> { private void useSite(final ActionEvent event) {
MPSite<?> site = getSite();
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 ))
return;
site = ((MPNewSite) site).addTo( user );
}
boolean loginResult = (copyLoginKeyStroke.getModifiers() & event.getModifiers()) != 0;
MPSite<?> fsite = site;
Res.ui( getSiteResult( site, loginResult ), result -> {
if (result == null) if (result == null)
return; return;
if (site instanceof MPFileSite) if (fsite instanceof MPFileSite)
((MPFileSite) site).use(); ((MPFileSite) fsite).use();
Transferable clipboardContents = new StringSelection( result ); copyResult( result );
Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null );
Res.ui( () -> {
Window window = SwingUtilities.windowForComponent( UserContentPanel.this );
if (window != null)
window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_CLOSING ) );
} );
} ); } );
} }
private void showSiteResult(@Nullable final MPSite<?> site) { private void setShowLogin(final boolean showLogin) {
showSiteResult( site, null ); if (showLogin == this.showLogin)
}
private void showSiteResult(@Nullable final MPSite<?> site, @Nullable final Consumer<String> resultCallback) {
if (site == null) {
if (resultCallback != null)
resultCallback.accept( null );
Res.ui( () -> {
passwordLabel.setText( " " );
passwordField.setText( " " );
settingsButton.setEnabled( false );
deleteButton.setEnabled( false );
} );
return; return;
this.showLogin = showLogin;
showSiteItem( sitesModel.getSelectedItem() );
} }
Res.job( () -> { private void showSiteItem(@Nullable final MPQuery.Result<? extends MPSite<?>> item) {
try { MPSite<?> site = (item != null)? item.getValue(): null;
String result = site.getResult(); Res.ui( getSiteResult( site, showLogin ), result -> {
if (resultCallback != null) if (!showLogin && (site != null))
resultCallback.accept( result ); resultLabel.setText( (result != null)? strf( "Your password for %s:", site.getSiteName() ): " " );
else if (showLogin && (site != null))
resultLabel.setText( (result != null)? strf( "Your login for %s:", site.getSiteName() ): " " );
Res.ui( () -> { if ((result == null) || result.isEmpty())
passwordLabel.setText( strf( "Your password for %s:", site.getSiteName() ) ); resultField.setText( " " );
passwordField.setText( result ); else if (!showLogin && user.getPreferences().isHidePasswords())
settingsButton.setEnabled( true ); resultField.setText( EACH_CHARACTER.matcher( result ).replaceAll( "" ) );
deleteButton.setEnabled( true ); else
resultField.setText( result );
settingsButton.setEnabled( result != null );
questionsButton.setEnabled( result != null );
editButton.setEnabled( result != null );
keyButton.setEnabled( result != null );
deleteButton.setEnabled( result != null );
} ); } );
} }
private ListenableFuture<String> getSiteResult(@Nullable final MPSite<?> site, final boolean loginResult) {
return Res.job( () -> {
try {
if (site != null)
return loginResult? site.getLogin(): site.getResult();
}
catch (final MPKeyUnavailableException | MPAlgorithmException e) { catch (final MPKeyUnavailableException | MPAlgorithmException e) {
logger.err( e, "While resolving password for: %s", site ); logger.err( e, "While resolving password for: %s", site );
} }
return null;
} ); } );
} }
private void useQuestion(final ActionEvent event) {
MPQuestion question = getQuestion();
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 ))
return;
question = question.getSite().addQuestion( question.getKeyword() );
}
MPQuestion fquestion = question;
Res.ui( getQuestionResult( question ), result -> {
if (result == null)
return;
if (fquestion instanceof MPFileQuestion)
((MPFileQuestion) fquestion).use();
copyResult( result );
} );
}
private void showQuestionItem(@Nullable final MPQuery.Result<? extends MPQuestion> item) {
MPQuestion question = (item != null)? item.getValue(): null;
Res.ui( getQuestionResult( question ), answer -> {
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 ListenableFuture<String> getQuestionResult(@Nullable final MPQuestion question) {
return 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;
} );
}
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 );
setShowLogin( false );
} );
}
@Nullable
private MPSite<?> getSite() {
MPQuery.Result<? extends MPSite<?>> selectedSite = sitesModel.getSelectedItem();
if (selectedSite == null)
return null;
return selectedSite.getValue();
}
@Nullable
private MPQuestion getQuestion() {
MPQuery.Result<? extends MPQuestion> selectedQuestion = questionsModel.getSelectedItem();
if (selectedQuestion == null)
return null;
return selectedQuestion.getValue();
}
@Override @Override
public void keyTyped(final KeyEvent event) { public void keyTyped(final KeyEvent event) {
} }
@@ -673,27 +1027,33 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
sitesList.dispatchEvent( event ); sitesList.dispatchEvent( event );
} }
private synchronized void updateSites(@Nullable final String query) { private synchronized void updateSites(@Nullable final String queryText) {
if (updateSitesJob != null) if (updateSitesJob != null)
updateSitesJob.cancel( true ); updateSitesJob.cancel( true );
updateSitesJob = Res.job( () -> { updateSitesJob = Res.job( () -> {
Collection<MPSite<?>> sites = new LinkedList<>(); MPQuery query = new MPQuery( queryText );
if (!Strings.isNullOrEmpty( query )) { Collection<MPQuery.Result<? extends MPSite<?>>> siteItems = new LinkedList<MPQuery.Result<? extends MPSite<?>>>(
sites.addAll( new LinkedList<>( user.findSites( query ) ) ); query.find( user.getSites(), MPSite::getSiteName ) );
if (sites.stream().noneMatch( site -> site.getSiteName().equalsIgnoreCase( query ) )) if (!Strings.isNullOrEmpty( queryText ))
sites.add( new MPNewSite( user, query ) ); if (siteItems.stream().noneMatch( MPQuery.Result::isExact )) {
MPQuery.Result<? extends MPSite<?>> selectedItem = sitesModel.getSelectedItem();
if ((selectedItem != null) && user.equals( selectedItem.getValue().getUser() ) &&
queryText.equals( selectedItem.getValue().getSiteName() ))
siteItems.add( selectedItem );
else
siteItems.add( MPQuery.Result.allOf( new MPNewSite( user, query.getQuery() ), query.getQuery() ) );
} }
Res.ui( () -> sitesModel.set( sites ) ); Res.ui( () -> sitesModel.set( siteItems ) );
} ); } );
} }
@Override @Override
public void onUserUpdated(final MPUser<?> user) { public void onUserUpdated(final MPUser<?> user) {
updateSites( queryField.getText() ); updateSites( queryField.getText() );
showSiteResult( sitesModel.getSelectedItem() ); showSiteItem( sitesModel.getSelectedItem() );
} }
@Override @Override
@@ -703,5 +1063,13 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
@Override @Override
public void onUserInvalidated(final MPUser<?> user) { public void onUserInvalidated(final MPUser<?> user) {
} }
@Override
public boolean dispatchKeyEvent(final KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT)
setShowLogin( e.isShiftDown() );
return false;
}
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,78 @@
package com.lyndir.masterpassword.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.MutableClassToInstanceMap;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.model.impl.Changeable;
import com.lyndir.masterpassword.model.impl.MPJSONAnyObject;
import java.io.File;
import java.io.IOException;
/**
* @author lhunath, 2018-10-14
*/
@SuppressWarnings("CallToSystemGetenv")
public class MPConfig extends MPJSONAnyObject {
private static final Logger logger = Logger.get( MPConfig.class );
private static final ClassToInstanceMap<MPConfig> instances = MutableClassToInstanceMap.create();
private static final File configFile = new File( rcDir(), "config.json" );
private final Changeable changeable = new Changeable() {
@Override
protected void onChanged() {
try {
objectMapper.writerWithDefaultPrettyPrinter().writeValue( configFile, MPConfig.this );
instances.clear();
}
catch (final IOException e) {
logger.err( e, "While saving config to: %s", configFile );
}
}
};
protected static synchronized <C extends MPConfig> C get(final Class<C> type) {
C instance = instances.getInstance( type );
if (instance == null)
if (configFile.exists())
try {
instances.putInstance( type, instance = objectMapper.readValue( configFile, type ) );
}
catch (final IOException e) {
logger.wrn( e, "While reading config file: %s", configFile );
}
if (instance == null)
try {
instance = type.getConstructor().newInstance();
}
catch (final ReflectiveOperationException e) {
throw logger.bug( e );
}
return instance;
}
protected void setChanged() {
changeable.setChanged();
}
public static MPConfig get() {
return get( MPConfig.class );
}
public static File rcDir() {
String rcDir = System.getenv( MPModelConstants.env_rcDir );
if (rcDir != null)
return new File( rcDir );
String home = System.getProperty( "user.home" );
if (home == null)
home = System.getenv( "HOME" );
return new File( home, ".mpw.d" );
}
}

View File

@@ -25,7 +25,7 @@ import org.joda.time.format.ISODateTimeFormat;
/** /**
* @author lhunath, 2016-10-29 * @author lhunath, 2016-10-29
*/ */
public final class MPConstants { public final class MPModelConstants {
/* Environment */ /* Environment */
@@ -33,6 +33,7 @@ public final class MPConstants {
* mpw: default path to look for run configuration files if the platform default is not desired. * mpw: default path to look for run configuration files if the platform default is not desired.
*/ */
public static final String env_rcDir = "MPW_RCDIR"; public static final String env_rcDir = "MPW_RCDIR";
/** /**
* mpw: permit automatic update checks. * mpw: permit automatic update checks.
*/ */

View File

@@ -0,0 +1,165 @@
package com.lyndir.masterpassword.model;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import java.util.*;
import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* @author lhunath, 2018-09-11
*/
public class MPQuery {
@Nonnull
private final String query;
public MPQuery(@Nullable final String query) {
this.query = (query != null)? query: "";
}
@Nonnull
public String getQuery() {
return query;
}
/**
* @return {@code true} if this query is contained wholly inside the given {@code key}.
*/
@Nonnull
public <V> Optional<Result<V>> matches(final V value, final CharSequence key) {
Result<V> result = Result.noneOf( value, key );
if (query.isEmpty())
return Optional.of( result );
if (key.length() == 0)
return Optional.empty();
// Consume query and key characters until one of them runs out, recording any matches against the result's key.
int q = 0, k = 0;
while ((q < query.length()) && (k < key.length())) {
if (query.charAt( q ) == key.charAt( k )) {
result.keyMatchedAt( k );
++q;
}
++k;
}
// If the match against the query broke before the end of the query, it failed.
return (q < query.length())? Optional.empty(): Optional.of( result );
}
/**
* @return Results for values that matched against the query, in the original values' order.
*/
@Nonnull
public <V> ImmutableCollection<Result<? extends V>> find(final Iterable<? extends V> values,
final Function<V, CharSequence> valueToKey) {
ImmutableList.Builder<Result<? extends V>> results = ImmutableList.builder();
for (final V value : values)
matches( value, valueToKey.apply( value ) ).ifPresent( results::add );
return results.build();
}
public static class Result<V> {
private final V value;
private final CharSequence key;
private final boolean[] keyMatches;
Result(final V value, final CharSequence key) {
this.value = value;
this.key = key;
keyMatches = new boolean[key.length()];
}
public static <T> Result<T> noneOf(final T value, final CharSequence key) {
return new Result<>( value, key );
}
public static <T> Result<T> allOf(final T value, final CharSequence key) {
Result<T> result = noneOf( value, key );
Arrays.fill( result.keyMatches, true );
return result;
}
@Nonnull
public V getValue() {
return value;
}
@Nonnull
public CharSequence getKey() {
return key;
}
public String getKeyAsHTML() {
return getKeyAsHTML( "u" );
}
@SuppressWarnings({ "MagicCharacter", "HardcodedFileSeparator" })
public String getKeyAsHTML(final String mark) {
String closeMark = mark.contains( " " )? mark.substring( 0, mark.indexOf( ' ' ) ): mark;
StringBuilder html = new StringBuilder();
boolean marked = false;
for (int i = 0; i < key.length(); ++i) {
if (keyMatches[i] && !marked) {
html.append( '<' ).append( mark ).append( '>' );
marked = true;
} else if (!keyMatches[i] && marked) {
html.append( '<' ).append( '/' ).append( closeMark ).append( '>' );
marked = false;
}
html.append( key.charAt( i ) );
}
if (marked)
html.append( '<' ).append( '/' ).append( closeMark ).append( '>' );
return html.toString();
}
public boolean[] getKeyMatches() {
return keyMatches.clone();
}
public boolean isExact() {
for (final boolean keyMatch : keyMatches)
if (!keyMatch)
return false;
return true;
}
private void keyMatchedAt(final int k) {
keyMatches[k] = true;
}
@Override
public boolean equals(final Object o) {
if (!(o instanceof Result))
return false;
Result<?> r = (Result<?>) o;
return Objects.equals( value, r.value ) && Objects.equals( key, r.key ) && Arrays.equals( keyMatches, r.keyMatches );
}
@Override
public int hashCode() {
return getValue().hashCode();
}
@Override
public String toString() {
return strf( "{Result: %s}", key );
}
}
}

View File

@@ -40,7 +40,14 @@ public interface MPQuestion extends Comparable<MPQuestion> {
void setType(MPResultType type); void setType(MPResultType type);
@Nonnull @Nullable
default String getAnswer()
throws MPKeyUnavailableException, MPAlgorithmException {
return getAnswer( null );
}
@Nullable
String getAnswer(@Nullable String state) String getAnswer(@Nullable String state)
throws MPKeyUnavailableException, MPAlgorithmException; throws MPKeyUnavailableException, MPAlgorithmException;

View File

@@ -57,35 +57,48 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
void setLoginType(@Nullable MPResultType loginType); void setLoginType(@Nullable MPResultType loginType);
@Nullable
default String getResult() default String getResult()
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
return getResult( MPKeyPurpose.Authentication ); return getResult( MPKeyPurpose.Authentication );
} }
@Nonnull @Nullable
default String getResult(final MPKeyPurpose keyPurpose) default String getResult(final MPKeyPurpose keyPurpose)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
return getResult( keyPurpose, null ); return getResult( keyPurpose, null );
} }
@Nonnull @Nullable
default String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext) default String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
return getResult( keyPurpose, keyContext, null ); return getResult( keyPurpose, keyContext, null );
} }
@Nonnull @Nullable
String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state) String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state)
throws MPKeyUnavailableException, MPAlgorithmException; throws MPKeyUnavailableException, MPAlgorithmException;
/**
* @see MPMasterKey#siteResult(String, MPAlgorithm, UnsignedInteger, MPKeyPurpose, String, MPResultType, String)
*/
@Nullable
String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext,
@Nullable UnsignedInteger counter, MPResultType type, @Nullable String state)
throws MPKeyUnavailableException, MPAlgorithmException;
@Nonnull @Nonnull
String getState(MPKeyPurpose keyPurpose, @Nullable String keyContext,
@Nullable UnsignedInteger counter, MPResultType type, String state)
throws MPKeyUnavailableException, MPAlgorithmException;
@Nullable
default String getLogin() default String getLogin()
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
return getLogin( null ); return getLogin( null );
} }
@Nonnull @Nullable
String getLogin(@Nullable String state) String getLogin(@Nullable String state)
throws MPKeyUnavailableException, MPAlgorithmException; throws MPKeyUnavailableException, MPAlgorithmException;
@@ -94,7 +107,11 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
@Nonnull @Nonnull
MPUser<?> getUser(); MPUser<?> getUser();
boolean addQuestion(Q question); @Nonnull
Q addQuestion(String keyword);
@Nonnull
Q addQuestion(Q question);
boolean deleteQuestion(Q question); boolean deleteQuestion(Q question);

View File

@@ -18,7 +18,6 @@
package com.lyndir.masterpassword.model; package com.lyndir.masterpassword.model;
import com.google.common.collect.ImmutableCollection;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import java.util.Collection; import java.util.Collection;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@@ -39,6 +38,9 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
@Nonnull @Nonnull
String getFullName(); String getFullName();
@Nonnull
MPUserPreferences getPreferences();
// - Algorithm // - Algorithm
@Nonnull @Nonnull
@@ -46,11 +48,6 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
void setAlgorithm(MPAlgorithm algorithm); void setAlgorithm(MPAlgorithm algorithm);
@Nullable
default MPResultType getDefaultType() {
return null;
}
@Nullable @Nullable
byte[] getKeyID(); byte[] getKeyID();
@@ -100,6 +97,7 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
// - Relations // - Relations
@Nonnull
S addSite(String siteName); S addSite(String siteName);
@Nonnull @Nonnull
@@ -110,12 +108,9 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
@Nonnull @Nonnull
Collection<S> getSites(); Collection<S> getSites();
@Nonnull void addListener(Listener listener);
ImmutableCollection<S> findSites(@Nullable String query);
boolean addListener(Listener listener); void removeListener(Listener listener);
boolean removeListener(Listener listener);
interface Listener { interface Listener {

View File

@@ -0,0 +1,19 @@
package com.lyndir.masterpassword.model;
import com.lyndir.masterpassword.MPResultType;
import javax.annotation.Nullable;
/**
* @author lhunath, 2018-10-13
*/
public interface MPUserPreferences {
MPResultType getDefaultType();
void setDefaultType(@Nullable MPResultType defaultType);
boolean isHidePasswords();
void setHidePasswords(boolean hidePasswords);
}

View File

@@ -7,7 +7,7 @@ import java.util.concurrent.Executors;
/** /**
* @author lhunath, 2018-07-08 * @author lhunath, 2018-07-08
*/ */
public class Changeable { public abstract class Changeable {
private static final ExecutorService changeExecutor = Executors.newSingleThreadExecutor(); private static final ExecutorService changeExecutor = Executors.newSingleThreadExecutor();
@@ -15,7 +15,9 @@ public class Changeable {
private Grouping grouping = Grouping.APPLY; private Grouping grouping = Grouping.APPLY;
private boolean changed; private boolean changed;
void setChanged() { protected abstract void onChanged();
public void setChanged() {
synchronized (mutex) { synchronized (mutex) {
if (changed) if (changed)
return; return;
@@ -37,9 +39,6 @@ public class Changeable {
} ); } );
} }
protected void onChanged() {
}
public void beginChanges() { public void beginChanges() {
synchronized (mutex) { synchronized (mutex) {
grouping = Grouping.BATCH; grouping = Grouping.BATCH;

View File

@@ -22,6 +22,7 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPQuestion; import com.lyndir.masterpassword.model.MPQuestion;
import com.lyndir.masterpassword.model.MPSite;
import java.util.Objects; import java.util.Objects;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -33,14 +34,23 @@ import org.jetbrains.annotations.NotNull;
*/ */
public abstract class MPBasicQuestion extends Changeable implements MPQuestion { public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
private final MPSite<?> site;
private final String keyword; private final String keyword;
private MPResultType type; private MPResultType type;
protected MPBasicQuestion(final String keyword, final MPResultType type) { protected MPBasicQuestion(final MPSite<?> site, final String keyword, final MPResultType type) {
this.site = site;
this.keyword = keyword; this.keyword = keyword;
this.type = type; this.type = type;
} }
@Nonnull
@Override
public MPSite<?> getSite() {
return site;
}
@Nonnull @Nonnull
@Override @Override
public String getKeyword() { public String getKeyword() {
@@ -55,14 +65,14 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
@Override @Override
public void setType(final MPResultType type) { public void setType(final MPResultType type) {
if (Objects.equals(this.type, type)) if (this.type == type)
return; return;
this.type = type; this.type = type;
setChanged(); setChanged();
} }
@Nonnull @Nullable
@Override @Override
public String getAnswer(@Nullable final String state) public String getAnswer(@Nullable final String state)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@@ -70,15 +80,10 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
return getSite().getResult( MPKeyPurpose.Recovery, getKeyword(), null, getType(), state ); return getSite().getResult( MPKeyPurpose.Recovery, getKeyword(), null, getType(), state );
} }
@Nonnull
@Override
public abstract MPBasicSite<?, ?> getSite();
@Override @Override
protected void onChanged() { protected void onChanged() {
super.onChanged(); if (site instanceof Changeable)
((Changeable) site).setChanged();
getSite().setChanged();
} }
@Override @Override

View File

@@ -35,9 +35,9 @@ import javax.annotation.Nullable;
public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> extends Changeable public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> extends Changeable
implements MPSite<Q> { implements MPSite<Q> {
private final Collection<Q> questions = new LinkedHashSet<>();
private final U user; private final U user;
private final String siteName; private final String siteName;
private final Collection<Q> questions = new TreeSet<>();
private MPAlgorithm algorithm; private MPAlgorithm algorithm;
private UnsignedInteger counter; private UnsignedInteger counter;
@@ -55,17 +55,20 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
this.siteName = siteName; this.siteName = siteName;
this.algorithm = (algorithm != null)? algorithm: this.user.getAlgorithm(); this.algorithm = (algorithm != null)? algorithm: this.user.getAlgorithm();
this.counter = (counter != null)? counter: this.algorithm.mpw_default_counter(); this.counter = (counter != null)? counter: this.algorithm.mpw_default_counter();
this.resultType = (resultType != null)? resultType: this.resultType = (resultType != null)? resultType: this.user.getPreferences().getDefaultType();
ifNotNullElse( this.user.getDefaultType(), this.algorithm.mpw_default_result_type() );
this.loginType = (loginType != null)? loginType: this.algorithm.mpw_default_login_type(); this.loginType = (loginType != null)? loginType: this.algorithm.mpw_default_login_type();
} }
// - Meta
@Nonnull @Nonnull
@Override @Override
public String getSiteName() { public String getSiteName() {
return siteName; return siteName;
} }
// - Algorithm
@Nonnull @Nonnull
@Override @Override
public MPAlgorithm getAlgorithm() { public MPAlgorithm getAlgorithm() {
@@ -104,7 +107,7 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
@Override @Override
public void setResultType(final MPResultType resultType) { public void setResultType(final MPResultType resultType) {
if (Objects.equals( this.resultType, resultType )) if (this.resultType == resultType)
return; return;
this.resultType = resultType; this.resultType = resultType;
@@ -119,14 +122,14 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
@Override @Override
public void setLoginType(@Nullable final MPResultType loginType) { public void setLoginType(@Nullable final MPResultType loginType) {
if (Objects.equals( this.loginType, loginType )) if (this.loginType == loginType)
return; return;
this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() ); this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() );
setChanged(); setChanged();
} }
@Nonnull @Nullable
@Override @Override
public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nullable final String state) public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nullable final String state)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@@ -134,7 +137,9 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
return getResult( keyPurpose, keyContext, getCounter(), getResultType(), state ); return getResult( keyPurpose, keyContext, getCounter(), getResultType(), state );
} }
protected String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nullable
@Override
public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
@Nullable final UnsignedInteger counter, final MPResultType type, @Nullable final String state) @Nullable final UnsignedInteger counter, final MPResultType type, @Nullable final String state)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@@ -143,7 +148,9 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
keyPurpose, keyContext, type, state ); keyPurpose, keyContext, type, state );
} }
protected String getState(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nonnull
@Override
public String getState(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
@Nullable final UnsignedInteger counter, final MPResultType type, final String state) @Nullable final UnsignedInteger counter, final MPResultType type, final String state)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@@ -152,7 +159,7 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
keyPurpose, keyContext, type, state ); keyPurpose, keyContext, type, state );
} }
@Nonnull @Nullable
@Override @Override
public String getLogin(@Nullable final String state) public String getLogin(@Nullable final String state)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@@ -160,13 +167,21 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
return getResult( MPKeyPurpose.Identification, null, null, getLoginType(), state ); return getResult( MPKeyPurpose.Identification, null, null, getLoginType(), state );
} }
// - Relations
@Nonnull
@Override @Override
public boolean addQuestion(final Q question) { public U getUser() {
if (!questions.add( question )) return user;
return false; }
@Nonnull
@Override
public Q addQuestion(final Q question) {
questions.add( question );
setChanged(); setChanged();
return true; return question;
} }
@Override @Override
@@ -184,16 +199,8 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
return Collections.unmodifiableCollection( questions ); return Collections.unmodifiableCollection( questions );
} }
@Nonnull
@Override
public U getUser() {
return user;
}
@Override @Override
protected void onChanged() { protected void onChanged() {
super.onChanged();
if (user instanceof Changeable) if (user instanceof Changeable)
((Changeable) user).setChanged(); ((Changeable) user).setChanged();
} }

View File

@@ -20,8 +20,6 @@ package com.lyndir.masterpassword.model.impl;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*; import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableSortedSet;
import com.lyndir.lhunath.opal.system.CodeUtils; import com.lyndir.lhunath.opal.system.CodeUtils;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
@@ -47,7 +45,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
@Nullable @Nullable
protected MPMasterKey masterKey; protected MPMasterKey masterKey;
private final Map<String, S> sites = new LinkedHashMap<>(); private final Set<S> sites = new TreeSet<>();
protected MPBasicUser(final String fullName, final MPAlgorithm algorithm) { protected MPBasicUser(final String fullName, final MPAlgorithm algorithm) {
this( 0, fullName, algorithm ); this( 0, fullName, algorithm );
@@ -79,6 +77,12 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
return fullName; return fullName;
} }
@Nonnull
@Override
public MPUserPreferences getPreferences() {
return new MPBasicUserPreferences<MPBasicUser<?>>( this );
}
@Nonnull @Nonnull
@Override @Override
public MPAlgorithm getAlgorithm() { public MPAlgorithm getAlgorithm() {
@@ -177,7 +181,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
@Nonnull @Nonnull
@Override @Override
public S addSite(final S site) { public S addSite(final S site) {
sites.put( site.getSiteName(), site ); sites.add( site );
setChanged(); setChanged();
return site; return site;
@@ -185,7 +189,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
@Override @Override
public boolean deleteSite(final MPSite<?> site) { public boolean deleteSite(final MPSite<?> site) {
if (!sites.values().remove( site )) if (!sites.remove( site ))
return false; return false;
setChanged(); setChanged();
@@ -195,35 +199,21 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
@Nonnull @Nonnull
@Override @Override
public Collection<S> getSites() { public Collection<S> getSites() {
return Collections.unmodifiableCollection( sites.values() ); return Collections.unmodifiableCollection( sites );
}
@Nonnull
@Override
public ImmutableCollection<S> findSites(@Nullable final String query) {
ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder();
if (query != null)
for (final S site : getSites())
if (site.getSiteName().startsWith( query ))
results.add( site );
return results.build();
} }
@Override @Override
public boolean addListener(final Listener listener) { public void addListener(final Listener listener) {
return listeners.add( listener ); listeners.add( listener );
} }
@Override @Override
public boolean removeListener(final Listener listener) { public void removeListener(final Listener listener) {
return listeners.remove( listener ); listeners.remove( listener );
} }
@Override @Override
protected void onChanged() { protected void onChanged() {
super.onChanged();
for (final Listener listener : listeners) for (final Listener listener : listeners)
listener.onUserUpdated( this ); listener.onUserUpdated( this );
} }

View File

@@ -0,0 +1,46 @@
package com.lyndir.masterpassword.model.impl;
import com.lyndir.masterpassword.MPResultType;
import com.lyndir.masterpassword.model.MPUserPreferences;
import javax.annotation.Nullable;
/**
* @author lhunath, 2018-10-13
*/
public class MPBasicUserPreferences<U extends MPBasicUser<?>> implements MPUserPreferences {
private final U user;
@Nullable
private MPResultType defaultType;
private boolean hidePasswords;
public MPBasicUserPreferences(final U user) {
this.user = user;
}
protected U getUser() {
return user;
}
@Override
public MPResultType getDefaultType() {
return (defaultType != null)? defaultType: user.getAlgorithm().mpw_default_result_type();
}
@Override
public void setDefaultType(@Nullable final MPResultType defaultType) {
this.defaultType = defaultType;
}
@Override
public boolean isHidePasswords() {
return hidePasswords;
}
@Override
public void setHidePasswords(final boolean hidePasswords) {
this.hidePasswords = hidePasswords;
}
}

View File

@@ -21,7 +21,6 @@ package com.lyndir.masterpassword.model.impl;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*; import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -30,16 +29,14 @@ import javax.annotation.Nullable;
*/ */
public class MPFileQuestion extends MPBasicQuestion { public class MPFileQuestion extends MPBasicQuestion {
private final MPFileSite site;
@Nullable @Nullable
private String answerState; private String answerState;
@SuppressWarnings("TypeMayBeWeakened")
public MPFileQuestion(final MPFileSite site, final String keyword, public MPFileQuestion(final MPFileSite site, final String keyword,
@Nullable final MPResultType type, @Nullable final String answerState) { @Nullable final MPResultType type, @Nullable final String answerState) {
super( keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) ); super( site, keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) );
this.site = site;
this.answerState = answerState; this.answerState = answerState;
} }
@@ -48,6 +45,8 @@ public class MPFileQuestion extends MPBasicQuestion {
return answerState; return answerState;
} }
@Nullable
@Override
public String getAnswer() public String getAnswer()
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
return getAnswer( answerState ); return getAnswer( answerState );
@@ -66,9 +65,8 @@ public class MPFileQuestion extends MPBasicQuestion {
setChanged(); setChanged();
} }
@Nonnull public void use() {
@Override if (getSite() instanceof MPFileSite)
public MPFileSite getSite() { ((MPFileSite) getSite()).use();
return site;
} }
} }

View File

@@ -93,7 +93,7 @@ public class MPFileSite extends MPBasicSite<MPFileUser, MPFileQuestion> {
setChanged(); setChanged();
} }
@Nonnull @Nullable
@Override @Override
public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext) public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@@ -101,7 +101,7 @@ public class MPFileSite extends MPBasicSite<MPFileUser, MPFileQuestion> {
return getResult( keyPurpose, keyContext, getResultState() ); return getResult( keyPurpose, keyContext, getResultState() );
} }
@Nonnull @Nullable
@Override @Override
public String getLogin() public String getLogin()
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@@ -145,6 +145,12 @@ public class MPFileSite extends MPBasicSite<MPFileUser, MPFileQuestion> {
setChanged(); setChanged();
} }
@Nonnull
@Override
public MPFileQuestion addQuestion(final String keyword) {
return addQuestion( new MPFileQuestion( this, keyword, null, null ) );
}
@Override @Override
public int compareTo(@Nonnull final MPSite<?> o) { public int compareTo(@Nonnull final MPSite<?> o) {
int comparison = (o instanceof MPFileSite)? ((MPFileSite) o).getLastUsed().compareTo( getLastUsed() ): 0; int comparison = (o instanceof MPFileSite)? ((MPFileSite) o).getLastUsed().compareTo( getLastUsed() ): 0;

View File

@@ -20,11 +20,9 @@ package com.lyndir.masterpassword.model.impl;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import com.lyndir.masterpassword.model.*;
import com.lyndir.masterpassword.model.MPUser;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Objects;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.joda.time.Instant; import org.joda.time.Instant;
@@ -41,44 +39,48 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
@Nullable @Nullable
private byte[] keyID; private byte[] keyID;
private File path; private File file;
private MPMarshalFormat format; private MPMarshalFormat format;
private MPMarshaller.ContentMode contentMode; private MPMarshaller.ContentMode contentMode;
private MPResultType defaultType;
private ReadableInstant lastUsed; private ReadableInstant lastUsed;
private boolean complete; private boolean complete;
private final MPFileUserPreferences preferences;
@Nullable @Nullable
public static MPFileUser load(final File file) public static MPFileUser load(final File file)
throws IOException, MPMarshalException { throws IOException, MPMarshalException {
for (final MPMarshalFormat format : MPMarshalFormat.values()) for (final MPMarshalFormat format : MPMarshalFormat.values())
if (file.getName().endsWith( format.fileSuffix() )) if (format.matches( file ))
return format.unmarshaller().readUser( file ); return format.unmarshaller().readUser( file );
return null; return null;
} }
public MPFileUser(final String fullName, final File path) { public MPFileUser(final String fullName, final File location) {
this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm(), path ); this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm(), location );
} }
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final File path) { public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final File location) {
this( fullName, keyID, algorithm, 0, null, new Instant(), this( fullName, keyID, algorithm, 0, null, new Instant(), false,
MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, path ); MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, location );
} }
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final int avatar,
final int avatar, @Nullable final MPResultType defaultType, final ReadableInstant lastUsed, @Nullable final MPResultType defaultType, final ReadableInstant lastUsed, final boolean hidePasswords,
final MPMarshaller.ContentMode contentMode, final MPMarshalFormat format, final File path) { final MPMarshaller.ContentMode contentMode, final MPMarshalFormat format, final File location) {
super( avatar, fullName, algorithm ); super( avatar, fullName, algorithm );
this.keyID = (keyID != null)? keyID.clone(): null; this.keyID = (keyID != null)? keyID.clone(): null;
this.defaultType = (defaultType != null)? defaultType: algorithm.mpw_default_result_type();
this.lastUsed = lastUsed; this.lastUsed = lastUsed;
this.path = path; this.preferences = new MPFileUserPreferences( this, defaultType, hidePasswords );
this.format = format; this.format = format;
this.contentMode = contentMode; this.contentMode = contentMode;
if (location.isDirectory())
this.file = new File( location, getFullName() + getFormat().fileSuffix() );
else
this.file = location;
} }
@Nullable @Nullable
@@ -87,8 +89,10 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
return (keyID == null)? null: keyID.clone(); return (keyID == null)? null: keyID.clone();
} }
public void setPath(final File path) { @Nonnull
this.path = path; @Override
public MPUserPreferences getPreferences() {
return preferences;
} }
@Override @Override
@@ -115,39 +119,18 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
return format; return format;
} }
public void setFormat(final MPMarshalFormat format) {
if (Objects.equals( this.format, format ))
return;
this.format = format;
setChanged();
}
public MPMarshaller.ContentMode getContentMode() { public MPMarshaller.ContentMode getContentMode() {
return contentMode; return contentMode;
} }
public void setContentMode(final MPMarshaller.ContentMode contentMode) { public void setContentMode(final MPMarshaller.ContentMode contentMode) {
if (Objects.equals( this.contentMode, contentMode )) if (this.contentMode == contentMode)
return; return;
this.contentMode = contentMode; this.contentMode = contentMode;
setChanged(); setChanged();
} }
@Override
public MPResultType getDefaultType() {
return defaultType;
}
public void setDefaultType(final MPResultType defaultType) {
if (Objects.equals( this.defaultType, defaultType ))
return;
this.defaultType = defaultType;
setChanged();
}
public ReadableInstant getLastUsed() { public ReadableInstant getLastUsed() {
return lastUsed; return lastUsed;
} }
@@ -166,7 +149,47 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
} }
public File getFile() { public File getFile() {
return new File( path, getFullName() + getFormat().fileSuffix() ); return file;
}
public void migrateTo(final MPMarshalFormat format) {
if (this.format == format)
return;
migrateTo( file.getParentFile(), format );
}
public void migrateTo(final File path) {
migrateTo( path, format );
}
/**
* Move the file for this user to the given path using a standard user-derived filename (ie. {@code [full name].[format suffix]})
*
* The user's old file is either moved to the new or deleted. If the user's file was already at the destination, it doesn't change.
* If a file already exists at the destination, it is overwritten.
*/
public void migrateTo(final File path, final MPMarshalFormat newFormat) {
MPMarshalFormat oldFormat = format;
File oldFile = file, newFile = new File( path, getFullName() + newFormat.fileSuffix() );
// If the format hasn't changed, migrate by moving the file: the contents doesn't need to change.
if ((oldFormat == newFormat) && !oldFile.equals( newFile ) && oldFile.exists())
if (!oldFile.renameTo( newFile ))
logger.wrn( "Couldn't move %s to %s for migration.", oldFile, newFile );
this.format = newFormat;
this.file = newFile;
// If the format has changed, save the new format into the new file and delete the old file. Revert if the user cannot be saved.
if ((oldFormat != newFormat) && !oldFile.equals( newFile ))
if (save()) {
if (oldFile.exists() && !oldFile.delete())
logger.wrn( "Couldn't delete %s after migration.", oldFile );
} else {
this.format = oldFormat;
this.file = oldFile;
}
} }
@Override @Override
@@ -187,10 +210,16 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
} }
} }
public void save() { /**
* @return {@code false} if the user is not fully loaded (complete), authenticated, or an issue prevented the marshalling.
*/
public boolean save() {
if (!isComplete())
return false;
try { try {
if (isComplete())
getFormat().marshaller().marshall( this ); getFormat().marshaller().marshall( this );
return true;
} }
catch (final MPKeyUnavailableException e) { catch (final MPKeyUnavailableException e) {
logger.wrn( e, "Cannot write out changes for unauthenticated user: %s.", this ); logger.wrn( e, "Cannot write out changes for unauthenticated user: %s.", this );
@@ -198,6 +227,8 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
catch (final IOException | MPMarshalException | MPAlgorithmException e) { catch (final IOException | MPMarshalException | MPAlgorithmException e) {
logger.err( e, "Unable to write out changes for user: %s", this ); logger.err( e, "Unable to write out changes for user: %s", this );
} }
return false;
} }
@Override @Override
@@ -207,6 +238,7 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
super.reset(); super.reset();
} }
@Nonnull
@Override @Override
public MPFileSite addSite(final String siteName) { public MPFileSite addSite(final String siteName) {
return addSite( new MPFileSite( this, siteName ) ); return addSite( new MPFileSite( this, siteName ) );

View File

@@ -18,11 +18,9 @@
package com.lyndir.masterpassword.model.impl; package com.lyndir.masterpassword.model.impl;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.ImmutableSortedSet;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.model.MPConstants; import com.lyndir.masterpassword.model.MPConfig;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
@@ -39,18 +37,7 @@ public class MPFileUserManager {
@SuppressWarnings("UnusedDeclaration") @SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MPFileUserManager.class ); private static final Logger logger = Logger.get( MPFileUserManager.class );
private static final MPFileUserManager instance; private static final MPFileUserManager instance = create( MPConfig.get().rcDir() );
static {
String rcDir = System.getenv( MPConstants.env_rcDir );
if (rcDir != null)
instance = create( new File( rcDir ) );
else {
String home = ifNotNullElseNullable( System.getProperty( "user.home" ), System.getenv( "HOME" ) );
instance = create( new File( home, ".mpw.d" ) );
}
}
private final Collection<Listener> listeners = new CopyOnWriteArraySet<>(); private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
private final Map<String, MPFileUser> userByName = new HashMap<>(); private final Map<String, MPFileUser> userByName = new HashMap<>();
@@ -66,6 +53,7 @@ public class MPFileUserManager {
protected MPFileUserManager(final File path) { protected MPFileUserManager(final File path) {
this.path = path; this.path = path;
reload();
} }
public void reload() { public void reload() {
@@ -77,14 +65,13 @@ public class MPFileUserManager {
return; return;
} }
for (final MPMarshalFormat format : MPMarshalFormat.values())
for (final File file : pathFiles) for (final File file : pathFiles)
if (format.matches( file ))
try { try {
MPFileUser user = MPFileUser.load( file ); MPFileUser user = MPFileUser.load( file );
if (user != null) { if (user != null)
MPFileUser previousUser = userByName.put( user.getFullName(), user ); add( user );
if ((previousUser != null) && (previousUser.getFormat().ordinal() > user.getFormat().ordinal()))
userByName.put( previousUser.getFullName(), previousUser );
}
} }
catch (final IOException | MPMarshalException e) { catch (final IOException | MPMarshalException e) {
logger.err( e, "Couldn't read user from: %s", file ); logger.err( e, "Couldn't read user from: %s", file );
@@ -98,12 +85,19 @@ public class MPFileUserManager {
} }
public MPFileUser add(final MPFileUser user) { public MPFileUser add(final MPFileUser user) {
user.setPath( getPath() ); // We migrate in two steps to allow the first to complete even if the user is not in the right state to complete the latter.
user.save(); user.migrateTo( getPath() );
user.migrateTo( MPMarshalFormat.DEFAULT );
MPFileUser oldUser = userByName.put( user.getFullName(), user ); MPFileUser oldUser = userByName.put( user.getFullName(), user );
if (oldUser != null) if (oldUser != null) {
oldUser.invalidate(); oldUser.invalidate();
// Delete old user, it is replaced by the new one.
if (!oldUser.getFile().equals( user.getFile() ) && oldUser.getFile().exists())
if (!oldUser.getFile().delete())
logger.err( "Couldn't delete file: %s, after replacing with: %s", oldUser.getFile(), user.getFile() );
}
fireUpdated(); fireUpdated();
return user; return user;
@@ -128,12 +122,13 @@ public class MPFileUserManager {
return ImmutableSortedSet.copyOf( userByName.values() ); return ImmutableSortedSet.copyOf( userByName.values() );
} }
public boolean addListener(final Listener listener) { public void addListener(final Listener listener) {
return listeners.add( listener ); if (listeners.add( listener ))
listener.onFilesUpdated( getFiles() );
} }
public boolean removeListener(final Listener listener) { public void removeListener(final Listener listener) {
return listeners.remove( listener ); listeners.remove( listener );
} }
private void fireUpdated() { private void fireUpdated() {
@@ -141,7 +136,6 @@ public class MPFileUserManager {
return; return;
ImmutableSortedSet<MPFileUser> files = getFiles(); ImmutableSortedSet<MPFileUser> files = getFiles();
for (final Listener listener : listeners) for (final Listener listener : listeners)
listener.onFilesUpdated( files ); listener.onFilesUpdated( files );
} }

View File

@@ -0,0 +1,37 @@
package com.lyndir.masterpassword.model.impl;
import com.lyndir.masterpassword.MPResultType;
import java.util.Objects;
import javax.annotation.Nullable;
/**
* @author lhunath, 2018-10-13
*/
public class MPFileUserPreferences extends MPBasicUserPreferences<MPFileUser> {
public MPFileUserPreferences(final MPFileUser user, @Nullable final MPResultType defaultType, final boolean hidePasswords) {
super( user );
setDefaultType( defaultType );
setHidePasswords( hidePasswords );
}
@Override
public void setDefaultType(@Nullable final MPResultType defaultType) {
if (getDefaultType() == defaultType)
return;
super.setDefaultType( defaultType );
getUser().setChanged();
}
@Override
public void setHidePasswords(final boolean hidePasswords) {
if (Objects.equals( isHidePasswords(), hidePasswords ))
return;
super.setHidePasswords( hidePasswords );
getUser().setChanged();
}
}

View File

@@ -25,7 +25,7 @@ import com.google.common.base.Charsets;
import com.google.common.io.CharSink; import com.google.common.io.CharSink;
import com.lyndir.masterpassword.MPAlgorithmException; import com.lyndir.masterpassword.MPAlgorithmException;
import com.lyndir.masterpassword.MPKeyUnavailableException; import com.lyndir.masterpassword.MPKeyUnavailableException;
import com.lyndir.masterpassword.model.MPConstants; import com.lyndir.masterpassword.model.MPModelConstants;
import java.io.*; import java.io.*;
import org.joda.time.Instant; import org.joda.time.Instant;
@@ -50,13 +50,13 @@ public class MPFlatMarshaller implements MPMarshaller {
content.append( "# \n" ); content.append( "# \n" );
content.append( "##\n" ); content.append( "##\n" );
content.append( "# Format: " ).append( FORMAT ).append( '\n' ); content.append( "# Format: " ).append( FORMAT ).append( '\n' );
content.append( "# Date: " ).append( MPConstants.dateTimeFormatter.print( new Instant() ) ).append( '\n' ); content.append( "# Date: " ).append( MPModelConstants.dateTimeFormatter.print( new Instant() ) ).append( '\n' );
content.append( "# User Name: " ).append( user.getFullName() ).append( '\n' ); content.append( "# User Name: " ).append( user.getFullName() ).append( '\n' );
content.append( "# Full Name: " ).append( user.getFullName() ).append( '\n' ); content.append( "# Full Name: " ).append( user.getFullName() ).append( '\n' );
content.append( "# Avatar: " ).append( user.getAvatar() ).append( '\n' ); content.append( "# Avatar: " ).append( user.getAvatar() ).append( '\n' );
content.append( "# Key ID: " ).append( user.exportKeyID() ).append( '\n' ); content.append( "# Key ID: " ).append( user.exportKeyID() ).append( '\n' );
content.append( "# Algorithm: " ).append( user.getAlgorithm().version().toInt() ).append( '\n' ); content.append( "# Algorithm: " ).append( user.getAlgorithm().version().toInt() ).append( '\n' );
content.append( "# Default Type: " ).append( user.getDefaultType().getType() ).append( '\n' ); content.append( "# Default Type: " ).append( user.getPreferences().getDefaultType().getType() ).append( '\n' );
content.append( "# Passwords: " ).append( user.getContentMode().name() ).append( '\n' ); content.append( "# Passwords: " ).append( user.getContentMode().name() ).append( '\n' );
content.append( "##\n" ); content.append( "##\n" );
content.append( "#\n" ); content.append( "#\n" );
@@ -72,7 +72,7 @@ public class MPFlatMarshaller implements MPMarshaller {
} }
content.append( strf( "%s %8d %8s %25s\t%25s\t%s\n", // content.append( strf( "%s %8d %8s %25s\t%25s\t%s\n", //
MPConstants.dateTimeFormatter.print( site.getLastUsed() ), // lastUsed MPModelConstants.dateTimeFormatter.print( site.getLastUsed() ), // lastUsed
site.getUses(), // uses site.getUses(), // uses
strf( "%d:%d:%d", // strf( "%d:%d:%d", //
site.getResultType().getType(), // type site.getResultType().getType(), // type

View File

@@ -25,8 +25,8 @@ import com.lyndir.lhunath.opal.system.CodeUtils;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ConversionUtils; import com.lyndir.lhunath.opal.system.util.ConversionUtils;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPConstants;
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
import com.lyndir.masterpassword.model.MPModelConstants;
import java.io.*; import java.io.*;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -56,6 +56,7 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
int mpVersion = 0, avatar = 0; int mpVersion = 0, avatar = 0;
boolean clearContent = false, headerStarted = false; boolean clearContent = false, headerStarted = false;
MPResultType defaultType = null; MPResultType defaultType = null;
Instant date = null;
//noinspection HardcodedLineSeparator //noinspection HardcodedLineSeparator
for (final String line : CharStreams.readLines( reader )) for (final String line : CharStreams.readLines( reader ))
@@ -66,10 +67,11 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
headerStarted = true; headerStarted = true;
else if ((fullName != null) && (keyID != null)) else if ((fullName != null) && (keyID != null))
// Ends the header. // Ends the header.
return new MPFileUser( fullName, keyID, MPAlgorithm.Version.fromInt( mpVersion ).getAlgorithm(), return new MPFileUser(
avatar, defaultType, new Instant( 0 ), fullName, keyID, MPAlgorithm.Version.fromInt( mpVersion ).getAlgorithm(), avatar, defaultType,
clearContent? MPMarshaller.ContentMode.VISIBLE: MPMarshaller.ContentMode.PROTECTED, date, false, clearContent? MPMarshaller.ContentMode.VISIBLE: MPMarshaller.ContentMode.PROTECTED,
MPMarshalFormat.Flat, file.getParentFile() ); MPMarshalFormat.Flat, file
);
} }
// Comment. // Comment.
@@ -91,6 +93,8 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
clearContent = "visible".equalsIgnoreCase( value ); clearContent = "visible".equalsIgnoreCase( value );
else if ("Default Type".equalsIgnoreCase( name )) else if ("Default Type".equalsIgnoreCase( name ))
defaultType = MPResultType.forType( ConversionUtils.toIntegerNN( value ) ); defaultType = MPResultType.forType( ConversionUtils.toIntegerNN( value ) );
else if ("Date".equalsIgnoreCase( name ))
date = MPModelConstants.dateTimeFormatter.parseDateTime( value ).toInstant();
} }
} }
} }
@@ -150,28 +154,32 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
MPFileSite site; MPFileSite site;
switch (importFormat) { switch (importFormat) {
case 0: case 0:
site = new MPFileSite( user, // site = new MPFileSite(
siteMatcher.group( 5 ), MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN( user, siteMatcher.group( 5 ),
MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN(
colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(), colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(),
user.getAlgorithm().mpw_default_counter(), user.getAlgorithm().mpw_default_counter(),
MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ), MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ),
clearContent? null: siteMatcher.group( 6 ), clearContent? null: siteMatcher.group( 6 ),
null, null, null, ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ), null, null, null, ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ),
MPConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() ); MPModelConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() );
if (clearContent) if (clearContent)
site.setSitePassword( site.getResultType(), siteMatcher.group( 6 ) ); site.setSitePassword( site.getResultType(), siteMatcher.group( 6 ) );
break; break;
case 1: case 1:
site = new MPFileSite( user, // site = new MPFileSite(
siteMatcher.group( 7 ), MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN( user, siteMatcher.group( 7 ),
MPAlgorithm.Version.fromInt( ConversionUtils.toIntegerNN(
colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(), colon.matcher( siteMatcher.group( 4 ) ).replaceAll( "" ) ) ).getAlgorithm(),
UnsignedInteger.valueOf( colon.matcher( siteMatcher.group( 5 ) ).replaceAll( "" ) ), UnsignedInteger.valueOf(
colon.matcher( siteMatcher.group( 5 ) ).replaceAll( "" ) ),
MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ), MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ),
clearContent? null: siteMatcher.group( 8 ), clearContent? null: siteMatcher.group( 8 ),
MPResultType.GeneratedName, clearContent? null: siteMatcher.group( 6 ), null, MPResultType.GeneratedName,
ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ), clearContent? null: siteMatcher.group( 6 ),
MPConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() ); null, ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ),
MPModelConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() );
if (clearContent) { if (clearContent) {
site.setSitePassword( site.getResultType(), siteMatcher.group( 8 ) ); site.setSitePassword( site.getResultType(), siteMatcher.group( 8 ) );
site.setLoginName( MPResultType.StoredPersonal, siteMatcher.group( 6 ) ); site.setLoginName( MPResultType.StoredPersonal, siteMatcher.group( 6 ) );

View File

@@ -19,6 +19,9 @@
package com.lyndir.masterpassword.model.impl; package com.lyndir.masterpassword.model.impl;
import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.core.util.Separators;
import com.fasterxml.jackson.databind.ObjectMapper;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.*; import java.util.*;
@@ -27,31 +30,59 @@ import java.util.*;
* @author lhunath, 2018-05-14 * @author lhunath, 2018-05-14
*/ */
@JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = MPJSONAnyObject.MPJSONEmptyValue.class) @JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = MPJSONAnyObject.MPJSONEmptyValue.class)
class MPJSONAnyObject { public class MPJSONAnyObject {
@SuppressWarnings("serial")
protected static final ObjectMapper objectMapper = new ObjectMapper() {
{
setDefaultPrettyPrinter( new DefaultPrettyPrinter() {
@Override
public DefaultPrettyPrinter withSeparators(final Separators separators) {
super.withSeparators( separators );
_objectFieldValueSeparatorWithSpaces = separators.getObjectFieldValueSeparator() + " ";
return this;
}
} );
setVisibility( PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE );
setVisibility( PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE );
}
};
@JsonAnySetter @JsonAnySetter
final Map<String, Object> any = new LinkedHashMap<>(); final Map<String, Object> any = new LinkedHashMap<>();
@JsonAnyGetter @JsonAnyGetter
public Map<String, Object> getAny() { public Map<String, Object> any() {
return Collections.unmodifiableMap( any ); return Collections.unmodifiableMap( any );
} }
@SuppressWarnings("unchecked")
public <V> V any(final String key) {
return (V) any.get( key );
}
@SuppressWarnings("EqualsAndHashcode") @SuppressWarnings("EqualsAndHashcode")
public static class MPJSONEmptyValue { public static class MPJSONEmptyValue {
@Override @Override
@SuppressWarnings({ "ChainOfInstanceofChecks", "Contract" }) @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
@SuppressFBWarnings({ "EQ_UNUSUAL", "EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS", "HE_EQUALS_USE_HASHCODE" }) @SuppressFBWarnings({ "EQ_UNUSUAL", "EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS", "HE_EQUALS_USE_HASHCODE" })
public boolean equals(final Object obj) { public boolean equals(final Object obj) {
return isEmpty( obj );
}
@SuppressWarnings({ "ChainOfInstanceofChecks", "ConstantConditions" })
private static boolean isEmpty(final Object obj) {
if (obj == null)
return true;
if (obj instanceof Collection<?>) if (obj instanceof Collection<?>)
return ((Collection<?>) obj).isEmpty(); return ((Collection<?>) obj).isEmpty();
if (obj instanceof Map<?, ?>) if (obj instanceof Map<?, ?>)
return ((Map<?, ?>) obj).isEmpty(); return ((Map<?, ?>) obj).isEmpty();
if (obj instanceof MPJSONFile.Site.Ext) if (obj instanceof MPJSONAnyObject)
return ((MPJSONAnyObject) obj).any.isEmpty(); return ((MPJSONAnyObject) obj).any.isEmpty() && (objectMapper.valueToTree( obj ).size() == 0);
return obj == null; return false;
} }
} }
} }

View File

@@ -28,8 +28,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.lyndir.lhunath.opal.system.CodeUtils; import com.lyndir.lhunath.opal.system.CodeUtils;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPConstants;
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
import com.lyndir.masterpassword.model.MPModelConstants;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File; import java.io.File;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@@ -44,22 +44,6 @@ import org.joda.time.Instant;
@SuppressFBWarnings("URF_UNREAD_FIELD") @SuppressFBWarnings("URF_UNREAD_FIELD")
public class MPJSONFile extends MPJSONAnyObject { public class MPJSONFile extends MPJSONAnyObject {
protected static final ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.setDefaultPrettyPrinter( new DefaultPrettyPrinter() {
private static final long serialVersionUID = 1;
@Override
public DefaultPrettyPrinter withSeparators(final Separators separators) {
super.withSeparators( separators );
_objectFieldValueSeparatorWithSpaces = separators.getObjectFieldValueSeparator() + " ";
return this;
}
} );
objectMapper.setVisibility( PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE );
}
MPJSONFile() { MPJSONFile() {
} }
@@ -70,16 +54,21 @@ public class MPJSONFile extends MPJSONAnyObject {
export = new Export(); export = new Export();
export.format = 1; export.format = 1;
export.redacted = modelUser.getContentMode().isRedacted(); export.redacted = modelUser.getContentMode().isRedacted();
export.date = MPConstants.dateTimeFormatter.print( new Instant() ); export.date = MPModelConstants.dateTimeFormatter.print( new Instant() );
// Section: "user" // Section: "user"
user = new User(); user = new User();
user.avatar = modelUser.getAvatar(); user.avatar = modelUser.getAvatar();
user.full_name = modelUser.getFullName(); user.full_name = modelUser.getFullName();
user.last_used = MPConstants.dateTimeFormatter.print( modelUser.getLastUsed() ); user.last_used = MPModelConstants.dateTimeFormatter.print( modelUser.getLastUsed() );
user.key_id = modelUser.exportKeyID(); user.key_id = modelUser.exportKeyID();
user.algorithm = modelUser.getAlgorithm().version(); user.algorithm = modelUser.getAlgorithm().version();
user.default_type = modelUser.getDefaultType(); user._ext_mpw = new User.Ext() {
{
default_type = modelUser.getPreferences().getDefaultType();
hide_passwords = modelUser.getPreferences().isHidePasswords();
}
};
// Section "sites" // Section "sites"
sites = new LinkedHashMap<>(); sites = new LinkedHashMap<>();
@@ -111,7 +100,7 @@ public class MPJSONFile extends MPJSONAnyObject {
site.login_type = modelSite.getLoginType(); site.login_type = modelSite.getLoginType();
site.uses = modelSite.getUses(); site.uses = modelSite.getUses();
site.last_used = MPConstants.dateTimeFormatter.print( modelSite.getLastUsed() ); site.last_used = MPModelConstants.dateTimeFormatter.print( modelSite.getLastUsed() );
site.questions = new LinkedHashMap<>(); site.questions = new LinkedHashMap<>();
for (final MPFileQuestion question : modelSite.getQuestions()) for (final MPFileQuestion question : modelSite.getQuestions())
@@ -130,8 +119,11 @@ public class MPJSONFile extends MPJSONAnyObject {
} }
} ); } );
site._ext_mpw = new Site.Ext(); site._ext_mpw = new Site.Ext() {
site._ext_mpw.url = modelSite.getUrl(); {
url = modelSite.getUrl();
}
};
} }
} }
@@ -140,10 +132,11 @@ public class MPJSONFile extends MPJSONAnyObject {
return new MPFileUser( return new MPFileUser(
user.full_name, CodeUtils.decodeHex( user.key_id ), algorithm, user.avatar, user.full_name, CodeUtils.decodeHex( user.key_id ), algorithm, user.avatar,
(user.default_type != null)? user.default_type: algorithm.mpw_default_result_type(), (user._ext_mpw != null)? user._ext_mpw.default_type: null,
(user.last_used != null)? MPConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(), (user.last_used != null)? MPModelConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(),
(user._ext_mpw != null) && user._ext_mpw.hide_passwords,
export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE, export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE,
MPMarshalFormat.JSON, file.getParentFile() MPMarshalFormat.JSON, file
); );
} }
@@ -157,7 +150,7 @@ public class MPJSONFile extends MPJSONAnyObject {
fileSite.type, export.redacted? fileSite.password: null, fileSite.type, export.redacted? fileSite.password: null,
fileSite.login_type, export.redacted? fileSite.login_name: null, fileSite.login_type, export.redacted? fileSite.login_name: null,
(fileSite._ext_mpw != null)? fileSite._ext_mpw.url: null, fileSite.uses, (fileSite._ext_mpw != null)? fileSite._ext_mpw.url: null, fileSite.uses,
(fileSite.last_used != null)? MPConstants.dateTimeFormatter.parseDateTime( fileSite.last_used ): new Instant() ); (fileSite.last_used != null)? MPModelConstants.dateTimeFormatter.parseDateTime( fileSite.last_used ): new Instant() );
if (!export.redacted) { if (!export.redacted) {
if (fileSite.password != null) if (fileSite.password != null)
@@ -170,7 +163,7 @@ public class MPJSONFile extends MPJSONAnyObject {
if (fileSite.questions != null) if (fileSite.questions != null)
for (final Map.Entry<String, Site.Question> questionEntry : fileSite.questions.entrySet()) { for (final Map.Entry<String, Site.Question> questionEntry : fileSite.questions.entrySet()) {
Site.Question fileQuestion = questionEntry.getValue(); Site.Question fileQuestion = questionEntry.getValue();
MPFileQuestion question = new MPFileQuestion( site, questionEntry.getKey(), MPFileQuestion question = new MPFileQuestion( site, ifNotNullElse( questionEntry.getKey(), "" ),
fileQuestion.type, export.redacted? fileQuestion.answer: null ); fileQuestion.type, export.redacted? fileQuestion.answer: null );
if (!export.redacted && (fileQuestion.answer != null)) if (!export.redacted && (fileQuestion.answer != null))
@@ -209,8 +202,17 @@ public class MPJSONFile extends MPJSONAnyObject {
String key_id; String key_id;
@Nullable @Nullable
MPAlgorithm.Version algorithm; MPAlgorithm.Version algorithm;
@Nullable
Ext _ext_mpw;
public static class Ext extends MPJSONAnyObject {
@Nullable @Nullable
MPResultType default_type; MPResultType default_type;
boolean hide_passwords;
}
} }

View File

@@ -18,6 +18,9 @@
package com.lyndir.masterpassword.model.impl; package com.lyndir.masterpassword.model.impl;
import java.io.File;
/** /**
* @author lhunath, 2017-09-20 * @author lhunath, 2017-09-20
* *
@@ -72,4 +75,8 @@ public enum MPMarshalFormat {
@SuppressWarnings("MethodReturnAlwaysConstant") @SuppressWarnings("MethodReturnAlwaysConstant")
public abstract String fileSuffix(); public abstract String fileSuffix();
public boolean matches(final File file) {
return file.getName().endsWith( fileSuffix() );
}
} }