2
0

Compare commits

...

39 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
73 changed files with 1995 additions and 799 deletions

1
.gitignore vendored
View File

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

View File

@@ -9,7 +9,7 @@ build_project:
- "( ./lib/bin/build_libsodium-macos clean && ./lib/bin/build_libsodium-macos )"
- "( ./lib/bin/build_libjson-c-macos clean && ./lib/bin/build_libjson-c-macos )"
- "( cd ./platform-independent/c/cli && ./clean && targets=all ./build && ./mpw-tests && ./mpw-cli-tests )"
- "( cd ./gradle && ./gradlew --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 macOS' clean build )"
tags:

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

@@ -5,21 +5,31 @@
<option name="myDefaultNotNull" value="javax.annotation.Nonnull" />
<option name="myNullables">
<value>
<list size="4">
<list size="9">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="4" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="4">
<list size="9">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="javax.validation.constraints.NotNull" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
</list>
</value>
</option>

View File

@@ -2,11 +2,11 @@ To build a release distribution:
Desktop:
STORE_PW=$(mpw masterpassword.keystore) KEY_PW_DESKTOP=$(mpw masterpassword-desktop) gradle clean masterpassword-gui:shadowJar
STORE_PW=$(mpw masterpassword.keystore) KEY_PW_DESKTOP=$(mpw masterpassword-desktop) gradle --no-daemon clean masterpassword-gui:shadowJar
Android:
STORE_PW=$(mpw masterpassword.keystore) KEY_PW_ANDROID=$(mpw masterpassword-android) gradle clean masterpassword-android:assembleRelease
STORE_PW=$(mpw masterpassword.keystore) KEY_PW_ANDROID=$(mpw masterpassword-android) gradle --no-daemon clean masterpassword-android:assembleRelease
Note:

View File

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

View File

@@ -36,7 +36,7 @@ _needs() {
IFS=: read pkg tools <<< "$spec"
IFS=, read -a tools <<< "${tools:-$pkg}"
for tool in "${tools[@]}"; do
hash "$tool" && continue 2
hash "$tool" 2>/dev/null && continue 2
done
echo >&2 "Missing: $pkg. Please install this package."
@@ -64,7 +64,7 @@ _initialize_needs() {
if [[ $platform = windows ]]; then
needs cmd
export VSINSTALLDIR="${VSINSTALLDIR:-$(cd "$(cygpath -F 0x002a)/Microsoft Visual Studio"/*/*/Common7/.. && pwd)}"
[[ -e "$VSINSTALLDIR/Common7/Tools/VsMSBuildCmd.bat" ]] || { echo >&2 "Missing: msbuild. Please install 'Build Tools for Visual Studio'."; 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
needs libtool:libtoolize,glibtoolize automake autoconf make
fi
@@ -202,7 +202,7 @@ _target_build() {
if [[ $platform = windows ]]; then
# I cannot for the life of me figure out how to pass this command directly into cmd.
printf '"%%VSINSTALLDIR%%\Common7\Tools\VsMSBuildCmd.bat" && msbuild /t:Rebuild /p:Configuration=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
rm -f .build.bat
else
@@ -278,7 +278,10 @@ _finalize_merge() {
# By default, this will run `make clean`.
finalize_clean() { _finalize_clean "$@"; }
_finalize_clean() {
[[ ! -e Makefile ]] || make -s clean
if [[ $platform = windows ]]; then :
else
[[ ! -e Makefile ]] || make -s clean
fi
}
# build <name> [<platform>]

View File

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

View File

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

View File

@@ -150,7 +150,7 @@
- (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.contentView.frame = cell.bounds;

View File

@@ -179,7 +179,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
- (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];
id item = self.dataSource[(NSUInteger)indexPath.section][(NSUInteger)indexPath.item];
if ([item isKindOfClass:[MPSiteEntity class]])

View File

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

View File

@@ -1,10 +1,14 @@
#!/usr/bin/env bash
#
# 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
#
# -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.
# By default, we only build the 'mpw' target.
# See targets_all for all possible targets as well as the features they support and require.
@@ -27,29 +31,53 @@ set -e
### 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=(
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-tests # C Master Password algorithm test suite (needs: mpw_sodium, mpw_xml).
)
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_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_xml=${mpw_xml:-1} # XML parsing (depends on libxml2).
# Default build flags.
cflags=( -O3 $CFLAGS )
ldflags=( $LDFLAGS )
# Default build flags
cflags=( -O3 $CFLAGS ); unset CFLAGS
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
cflags+=( -D"MP_VERSION=$mpw_version" )
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
@@ -132,18 +160,20 @@ mpw-tests() {
haslib() {
cc -x c "${ldflags[@]}" -l"$1" -o /dev/null - <<< 'int main() { return 0; }' &>/dev/null
}
cc() {
if hash llvm-gcc 2>/dev/null; then
cc() (
(( verbose )) && set -x
if { hash llvm-gcc; } 2>/dev/null; then
llvm-gcc "$@"
elif hash gcc 2>/dev/null; then
elif { hash gcc; } 2>/dev/null; then
gcc -std=c11 "$@"
elif hash clang 2>/dev/null; then
elif { hash clang; } 2>/dev/null; then
clang "$@"
else
echo >&2 "Need a compiler. Please install GCC or LLVM."
exit 1
fi
}
)
### DEPENDENCIES
@@ -156,7 +186,7 @@ use() {
for lib in "$lib" "$@"; do
haslib "$lib" && ldflags+=( -l"$lib" )
done
echo >&2 "INFO: Enabled $option (lib$lib)."
echo "INFO: Enabled $option (lib$lib)."
return 0
elif [[ $requisite == required ]]; then
@@ -174,7 +204,7 @@ use() {
exit 1
else
echo >&2 "INFO: $option is supported but not enabled."
echo "INFO: $option is supported but not enabled."
return 1
fi
}
@@ -192,13 +222,13 @@ use_mpw_json() {
}
use_mpw_xml() {
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
for target in "${targets_all[@]}"; do
if [[ ${targets:-$targets_default} == 'all' || " ${targets:-$targets_default} " = *" $target "* ]]; then
if [[ $targets == 'all' || " $targets " = *" $target "* ]]; then
echo
echo "Building target: $target..."
( "$target" "$@" )

View File

@@ -43,7 +43,7 @@ const char *mpw_getenv(const char *variableName) {
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 );
if (!askpass)
@@ -74,7 +74,7 @@ char *mpw_askpass(const char *prompt) {
}
close( pipes[1] );
char *answer = mpw_read_fd( pipes[0] );
const char *answer = mpw_read_fd( pipes[0] );
close( pipes[0] );
int status;
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 )) {
// Remove trailing newline.
if (answer[strlen( answer ) - 1] == '\n')
answer[strlen( answer ) - 1] = '\0';
mpw_replace_string( answer, mpw_strndup( answer, strlen( answer ) - 1 ) );
return answer;
}
@@ -97,71 +97,74 @@ char *mpw_askpass(const char *prompt) {
static const char *_mpw_getline(const char *prompt, bool silent) {
// Get answer from askpass.
char *answer = mpw_askpass( prompt );
const char *answer = mpw_askpass( prompt );
if (answer)
return answer;
#if MPW_COLOR
// Initialize a curses screen.
SCREEN *screen = newterm( NULL, stderr, stdin );
start_color();
init_pair( 1, COLOR_WHITE, COLOR_BLUE );
init_pair( 2, COLOR_BLACK, COLOR_WHITE );
int rows, cols;
getmaxyx( stdscr, rows, cols );
if (screen) {
start_color();
init_pair( 1, COLOR_WHITE, COLOR_BLUE );
init_pair( 2, COLOR_BLACK, COLOR_WHITE );
int rows, cols;
getmaxyx( stdscr, rows, cols );
// Display a dialog box.
int width = max( prompt? (int)strlen( prompt ): 0, MPW_MAX_INPUT ) + 6;
char *version = "mpw v" stringify_def( MP_VERSION );
mvprintw( rows - 1, (cols - (int)strlen( version )) / 2, "%s", version );
attron( A_BOLD );
color_set( 2, NULL );
mvprintw( rows / 2 - 1, (cols - width) / 2, "%s%*s%s", "*", width - 2, "", "*" );
mvprintw( rows / 2 - 1, (cols - (int)strlen( prompt )) / 2, "%s", prompt );
color_set( 1, NULL );
mvprintw( rows / 2 + 0, (cols - width) / 2, "%s%*s%s", "|", width - 2, "", "|" );
mvprintw( rows / 2 + 1, (cols - width) / 2, "%s%*s%s", "|", width - 2, "", "|" );
mvprintw( rows / 2 + 2, (cols - width) / 2, "%s%*s%s", "|", width - 2, "", "|" );
// Display a dialog box.
int width = max( prompt? (int)strlen( prompt ): 0, MPW_MAX_INPUT ) + 6;
char *version = "mpw v" stringify_def( MP_VERSION );
mvprintw( rows - 1, (cols - (int)strlen( version )) / 2, "%s", version );
attron( A_BOLD );
color_set( 2, NULL );
mvprintw( rows / 2 - 1, (cols - width) / 2, "%s%*s%s", "*", width - 2, "", "*" );
mvprintw( rows / 2 - 1, (cols - (int)strlen( prompt )) / 2, "%s", prompt );
color_set( 1, NULL );
mvprintw( rows / 2 + 0, (cols - width) / 2, "%s%*s%s", "|", width - 2, "", "|" );
mvprintw( rows / 2 + 1, (cols - width) / 2, "%s%*s%s", "|", width - 2, "", "|" );
mvprintw( rows / 2 + 2, (cols - width) / 2, "%s%*s%s", "|", width - 2, "", "|" );
// Read response.
color_set( 2, NULL );
attron( A_STANDOUT );
int result = ERR;
char str[MPW_MAX_INPUT + 1];
if (silent) {
mvprintw( rows / 2 + 1, (cols - 5) / 2, "[ * ]" );
refresh();
// Read response.
color_set( 2, NULL );
attron( A_STANDOUT );
int result = ERR;
char str[MPW_MAX_INPUT + 1];
if (silent) {
mvprintw( rows / 2 + 1, (cols - 5) / 2, "[ * ]" );
refresh();
noecho();
result = mvgetnstr( rows / 2 + 1, (cols - 1) / 2, str, MPW_MAX_INPUT );
echo();
} else {
mvprintw( rows / 2 + 1, (cols - (MPW_MAX_INPUT + 2)) / 2, "%*s", MPW_MAX_INPUT + 2, "" );
refresh();
noecho();
result = mvgetnstr( rows / 2 + 1, (cols - 1) / 2, str, MPW_MAX_INPUT );
echo();
}
else {
mvprintw( rows / 2 + 1, (cols - (MPW_MAX_INPUT + 2)) / 2, "%*s", MPW_MAX_INPUT + 2, "" );
refresh();
echo();
result = mvgetnstr( rows / 2 + 1, (cols - MPW_MAX_INPUT) / 2, str, MPW_MAX_INPUT );
echo();
result = mvgetnstr( rows / 2 + 1, (cols - MPW_MAX_INPUT) / 2, str, MPW_MAX_INPUT );
}
attrset( 0 );
endwin();
delscreen( screen );
return result == ERR? NULL: mpw_strndup( str, MPW_MAX_INPUT );
}
attrset( 0 );
endwin();
delscreen( screen );
#endif
return result == ERR? NULL: mpw_strndup( str, MPW_MAX_INPUT );
#else
// Get password from terminal.
fprintf( stderr, "%s ", prompt );
size_t bufSize = 0;
ssize_t lineSize = getline( &answer, &bufSize, stdin );
ssize_t lineSize = getline( (char **)&answer, &bufSize, stdin );
if (lineSize <= 1) {
mpw_free_string( &answer );
return NULL;
}
// Remove trailing newline.
answer[lineSize - 1] = '\0';
mpw_replace_string( answer, mpw_strndup( answer, (size_t)lineSize - 1 ) );
return answer;
#endif
}
const char *mpw_getline(const char *prompt) {
@@ -250,7 +253,7 @@ bool mpw_mkdirs(const char *filePath) {
return success;
}
char *mpw_read_fd(int fd) {
const char *mpw_read_fd(int fd) {
char *buf = NULL;
size_t blockSize = 4096, bufSize = 0, bufOffset = 0;
@@ -263,7 +266,7 @@ char *mpw_read_fd(int fd) {
return buf;
}
char *mpw_read_file(FILE *file) {
const char *mpw_read_file(FILE *file) {
if (!file)
return NULL;

View File

@@ -36,7 +36,7 @@ const char *mpw_getenv(const char *variableName);
/** Use the askpass program to prompt the user.
* @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.
* @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.
* @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.
* @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.
* @return A newly allocated string. */

View File

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

BIN
platform-independent/c/core/lib/windows/x86/mpw.dll Executable file → Normal file

Binary file not shown.

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. */
bool mpw_push_int(
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.
* 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.
@@ -124,26 +126,26 @@ bool mpw_push_int(
* @param deltaSize The amount to increase the buffer's size by.
* @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 ); })
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. */
#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 ); })
bool __mpw_free(
void **buffer, size_t bufferSize);
/** 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 ); })
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. */
#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__ ); })
bool __mpw_free_strings(
char **strings, ...);
/** Free a string after zero'ing its contents, then set the reference to the replacement string.
* 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
#undef mpw_realloc
#define mpw_realloc(buffer, bufferSize, deltaSize) \
@@ -158,6 +160,16 @@ bool __mpw_free_strings(
#define mpw_free_strings(strings, ...) \
__mpw_free_strings( (char **)strings, __VA_ARGS__ )
#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.

View File

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

View File

@@ -41,7 +41,7 @@ public enum MPResultType {
/**
* 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" ),
new MPTemplate( "axxxxxxxxxxxxxxxxxno" ) ), //
MPResultTypeClass.Template, 0x0 ),
@@ -49,7 +49,7 @@ public enum MPResultType {
/**
* 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" ),
new MPTemplate( "CvcvCvcvCvcvno" ), new MPTemplate( "CvccnoCvcvCvcv" ),
new MPTemplate( "CvccCvcvnoCvcv" ), new MPTemplate( "CvccCvcvCvcvno" ),
@@ -66,7 +66,7 @@ public enum MPResultType {
/**
* 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" ),
new MPTemplate( "CvcCvcno" ) ), //
MPResultTypeClass.Template, 0x2 ),
@@ -74,14 +74,14 @@ public enum MPResultType {
/**
* 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" ) ), //
MPResultTypeClass.Template, 0x3 ),
/**
* 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" ),
new MPTemplate( "aannaaan" ),
new MPTemplate( "aaannaaa" ) ), //
@@ -90,44 +90,44 @@ public enum MPResultType {
/**
* 21: 2798
*/
GeneratedPIN( "pin", "PIN Code", "2798", "4 numbers.", //
GeneratedPIN( "pin", "PIN Code", "2798", "4 numbers", //
ImmutableList.of( new MPTemplate( "nnnn" ) ), //
MPResultTypeClass.Template, 0x5 ),
/**
* 30: birsujano
*/
GeneratedName( "name", "Name", "birsujano", "9 letter name.", //
GeneratedName( "name", "Name", "birsujano", "9 letter name", //
ImmutableList.of( new MPTemplate( "cvccvcvcv" ) ), //
MPResultTypeClass.Template, 0xE ),
/**
* 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" ),
new MPTemplate( "cvc cvccvcvcv cvcv" ),
new MPTemplate( "cv cvccv cvc cvcvccv" ) ), //
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(), //
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(), //
MPResultTypeClass.Stateful, 0x1, MPSiteFeature.DevicePrivate ),
/**
* 4160: Derive a unique binary key.
*/
DeriveKey( "key", "Binary Key", null, "Encryption key.", //
DeriveKey( "key", "Binary Key", null, "Encryption key", //
ImmutableList.<MPTemplate>of(), //
MPResultTypeClass.Derive, 0x0, MPSiteFeature.Alternative );

View File

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

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

View File

@@ -31,7 +31,7 @@ shadowJar {
storepass: System.getenv( 'STORE_PW' ),
keypass: System.getenv( 'KEY_PW_DESKTOP' ),
preservelastmodified: 'true',
destdir: '.' )
signedJar: "${rootDir}/../public/site/${project.name}-${project.version}.jar" )
}
}

View File

@@ -1,36 +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;
import com.tulskiy.keymaster.common.Provider;
import java.awt.*;
/**
* @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 );
Provider.getCurrentProvider( true ).register( MPGuiConstants.ui_hotkey, hotKey -> open() );
}
public void open() {
Res.ui( () -> {
frame.setAlwaysOnTop( true );
frame.setVisible( true );
frame.setExtendedState( Frame.NORMAL );
frame.setAlwaysOnTop( false );
Platform.get().requestForeground();
} );
}
}

View File

@@ -19,6 +19,7 @@
package com.lyndir.masterpassword.gui;
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
import com.lyndir.masterpassword.model.MPConfig;
import com.lyndir.masterpassword.model.MPModelConstants;
@@ -26,15 +27,32 @@ import com.lyndir.masterpassword.model.MPModelConstants;
* @author lhunath, 2014-08-31
*/
@SuppressWarnings("CallToSystemGetenv")
public class Config {
public class MPGuiConfig extends MPConfig {
private static final Config instance = new Config();
public static Config get() {
return instance;
public static MPGuiConfig get() {
return get( MPGuiConfig.class );
}
Boolean checkForUpdates;
Boolean stayResident;
public boolean checkForUpdates() {
return ConversionUtils.toBoolean( System.getenv( MPModelConstants.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

@@ -24,8 +24,11 @@ import com.google.common.base.Charsets;
import com.google.common.io.ByteSource;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ObjectUtils;
import com.lyndir.masterpassword.gui.util.Components;
import com.lyndir.masterpassword.gui.util.*;
import com.lyndir.masterpassword.gui.view.MasterPasswordFrame;
import com.lyndir.masterpassword.model.MPUser;
import com.tulskiy.keymaster.common.Provider;
import java.awt.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.*;
@@ -43,25 +46,28 @@ import javax.swing.*;
public final class MasterPassword {
@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 final Provider keyMaster = Provider.getCurrentProvider( true );
private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
@Nullable
private MPUser<?> activeUser;
private MasterPasswordFrame frame;
@Nullable
private MPUser<?> activeUser;
public static MasterPassword get() {
return instance;
}
public boolean addListener(final Listener listener) {
return listeners.add( listener );
public void addListener(final Listener listener) {
if (listeners.add( listener ))
listener.onUserSelected( activeUser );
}
public boolean removeListener(final Listener listener) {
return listeners.remove( listener );
public void removeListener(final Listener listener) {
listeners.remove( listener );
}
public void activateUser(final MPUser<?> user) {
@@ -78,7 +84,42 @@ public final class MasterPassword {
return MasterPassword.class.getPackage().getImplementationVersion();
}
public void checkUpdate() {
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 {
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() {
@@ -108,25 +149,13 @@ public final class MasterPassword {
}
}
public static void main(final String... args) {
//Thread.setDefaultUncaughtExceptionHandler(
// (t, e) -> logger.bug( e, "Uncaught: %s", e.getLocalizedMessage() ) );
// Try and set the system look & feel, if available.
try {
UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
}
catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
}
// Create a platform-specific GUI and open it.
new GUI().open();
// Check online to see if this version has been superseded.
if (Config.get().checkForUpdates())
get().checkUpdate();
public void updateResidence() {
Platform.get().installAppForegroundHandler( get()::open );
Platform.get().installAppReopenHandler( get()::open );
keyMaster.register( MPGuiConstants.ui_hotkey, hotKey -> get().open() );
}
@SuppressWarnings("InterfaceMayBeAnnotatedFunctional")
public interface Listener {
void onUserSelected(@Nullable MPUser<?> user);

View File

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

View File

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

View File

@@ -18,6 +18,7 @@
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.event.ActionEvent;
@@ -32,8 +33,8 @@ import javax.annotation.Nullable;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.event.*;
import javax.swing.text.DefaultFormatterFactory;
import javax.swing.event.HyperlinkEvent;
import javax.swing.text.*;
import org.jetbrains.annotations.NonNls;
@@ -45,10 +46,10 @@ public abstract class Components {
private static final Logger logger = Logger.get( Components.class );
public static final float TEXT_SIZE_HEADING = 19f;
public static final float TEXT_SIZE_CONTROL = 13f;
public static final int SIZE_MARGIN = 12;
public static final int SIZE_PADDING = 8;
public static 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_PADDING = 8;
public static GradientPanel panel(final Component... components) {
GradientPanel panel = panel( BoxLayout.LINE_AXIS, null, components );
@@ -100,6 +101,7 @@ public abstract class Components {
public static int showDialog(@Nullable final Component owner, @Nullable final String title, final JOptionPane pane) {
JDialog dialog = pane.createDialog( owner, title );
dialog.setMinimumSize( new Dimension( 520, 0 ) );
dialog.setModalityType( Dialog.ModalityType.DOCUMENT_MODAL );
showDialog( dialog );
@@ -111,8 +113,13 @@ public abstract class Components {
if (options == null)
return (selectedValue instanceof Integer)? (Integer) selectedValue: JOptionPane.CLOSED_OPTION;
int option = Arrays.binarySearch( options, selectedValue );
return (option < 0)? JOptionPane.CLOSED_OPTION: option;
try {
int option = Arrays.binarySearch( options, selectedValue );
return (option < 0)? JOptionPane.CLOSED_OPTION: option;
}
catch (final ClassCastException ignored) {
return JOptionPane.CLOSED_OPTION;
}
}
@Nullable
@@ -143,7 +150,6 @@ public abstract class Components {
title, Dialog.ModalityType.DOCUMENT_MODAL );
dialog.setMinimumSize( new Dimension( 320, 0 ) );
dialog.setLocationRelativeTo( owner );
dialog.setLocationByPlatform( true );
dialog.setContentPane( content );
return showDialog( dialog );
@@ -155,13 +161,18 @@ public abstract class Components {
dialog.getRootPane().putClientProperty( "Window.style", "small" );
dialog.pack();
dialog.setLocationByPlatform( true );
dialog.setVisible( true );
return dialog;
}
public static JTextField textField() {
return new JTextField() {
return textField( null );
}
public static JTextField textField(@Nullable final Document document) {
return new JTextField( document, null, 0 ) {
{
setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) );
@@ -171,41 +182,30 @@ public abstract class Components {
@Override
public Dimension getMaximumSize() {
return new Dimension( Integer.MAX_VALUE, getPreferredSize().height );
return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE );
}
};
}
public static JTextField textField(@Nullable final String text, @Nullable final Consumer<String> selection) {
return new JTextField( text ) {
public static JTextField textField(@Nullable final String text, @Nullable final Consumer<String> change) {
return textField( new DocumentModel( new PlainDocument() ).selection( text, change ).getDocument() );
}
public static JTextArea textArea() {
return new JTextArea() {
{
setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) );
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
setAlignmentX( LEFT_ALIGNMENT );
if (selection != null)
getDocument().addDocumentListener( new DocumentListener() {
@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() );
}
} );
setLineWrap( true );
setRows( 3 );
}
@Override
public Dimension getMaximumSize() {
return new Dimension( Integer.MAX_VALUE, getPreferredSize().height );
return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE );
}
};
}
@@ -228,20 +228,27 @@ public abstract class Components {
public static <E> JList<E> list(final ListModel<E> model, final Function<E, String> valueTransformer) {
return new JList<E>( model ) {
{
setAlignmentX( LEFT_ALIGNMENT );
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
setCellRenderer( new DefaultListCellRenderer() {
@Override
@SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" })
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
final boolean isSelected, final boolean cellHasFocus) {
String label = valueTransformer.apply( (E) value );
super.getListCellRendererComponent(
list, valueTransformer.apply( (E) value ), index, isSelected, cellHasFocus );
list, Strings.isNullOrEmpty( label )? " ": label, index, isSelected, cellHasFocus );
setBorder( BorderFactory.createEmptyBorder( 2, 4, 2, 4 ) );
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
@@ -297,7 +304,6 @@ public abstract class Components {
public static JButton button(final Action action) {
return new JButton( action ) {
{
setFont( Res.fonts().controlFont( TEXT_SIZE_CONTROL ) );
setAlignmentX( LEFT_ALIGNMENT );
if (getText() == null) {
@@ -374,7 +380,7 @@ public abstract class Components {
public static JLabel heading(@Nullable final String heading, final int 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 );
}
@@ -409,7 +415,6 @@ public abstract class Components {
public static JLabel label(@Nullable final String label, final int horizontalAlignment) {
return new JLabel( label, horizontalAlignment ) {
{
setFont( Res.fonts().controlFont( TEXT_SIZE_CONTROL ) );
setAlignmentX( LEFT_ALIGNMENT );
}
@@ -421,11 +426,18 @@ public abstract class Components {
}
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 ) {
{
setFont( Res.fonts().controlFont( TEXT_SIZE_CONTROL ) );
setBackground( null );
setAlignmentX( LEFT_ALIGNMENT );
setSelected( selected );
if (selectionConsumer != null)
addItemListener( e -> selectionConsumer.accept( isSelected() ) );
}
};
}
@@ -437,17 +449,17 @@ public abstract class Components {
public static <E> JComboBox<E> comboBox(final E[] values, final Function<E, String> valueTransformer,
@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
return comboBox( 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,
@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,
@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) {
@@ -460,8 +472,9 @@ public abstract class Components {
@SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" })
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
final boolean isSelected, final boolean cellHasFocus) {
String label = valueTransformer.apply( (E) value );
super.getListCellRendererComponent(
list, valueTransformer.apply( (E) value ), index, isSelected, cellHasFocus );
list, Strings.isNullOrEmpty( label )? " ": label, index, isSelected, cellHasFocus );
setBorder( BorderFactory.createEmptyBorder( 0, 4, 0, 4 ) );
return this;

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 com.google.common.collect.Maps;
import com.google.common.io.Resources;
import com.google.common.util.concurrent.*;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.MPIdenticon;
import com.lyndir.masterpassword.gui.SwingExecutorService;
import java.awt.*;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.Map;
import java.util.concurrent.*;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import javax.swing.*;
import org.jetbrains.annotations.NonNls;
import org.joda.time.*;
@@ -80,6 +78,15 @@ public abstract class Res {
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) {
uiExecutor( immediate ).execute( job );
}
@@ -150,6 +157,14 @@ public abstract class Res {
return icon( "media/icon_settings.png" );
}
public Icon edit() {
return icon( "media/icon_edit.png" );
}
public Icon key() {
return icon( "media/icon_key.png" );
}
public Icon avatar(final int index) {
return icon( strf( "media/avatar-%d.png", index % avatars() ) );
}
@@ -166,88 +181,67 @@ public abstract class Res {
public static final class Fonts {
public Font emoticonsFont(final float size) {
return emoticonsRegular().deriveFont( size );
public Font emoticonsFont(final int size) {
return MPFont.emoticonsRegular.get( size );
}
public Font controlFont(final float size) {
return exoRegular().deriveFont( size );
public Font controlFont(final int size) {
return MPFont.exoRegular.get( size );
}
public Font valueFont(final float size) {
return sourceSansProRegular().deriveFont( size );
public Font valueFont(final int size) {
return MPFont.sourceSansProRegular.get( size );
}
public Font bigValueFont(final float size) {
return sourceSansProBlack().deriveFont( size );
public Font bigValueFont(final int size) {
return MPFont.sourceSansProBlack.get( size );
}
public Font emoticonsRegular() {
return font( "fonts/Emoticons-Regular.otf" );
}
private enum MPFont {
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" );
public Font sourceCodeProRegular() {
return font( "fonts/SourceCodePro-Regular.otf" );
}
private final String fontName;
private final String resourceName;
private boolean registered;
public Font sourceCodeProBlack() {
return font( "fonts/SourceCodePro-Bold.otf" );
}
MPFont(final String fontName, final String resourceName) {
this.fontName = fontName;
this.resourceName = resourceName;
}
public Font sourceSansProRegular() {
return font( "fonts/SourceSansPro-Regular.otf" );
}
Font get(final int size) {
return get( Font.PLAIN, size );
}
public Font sourceSansProBlack() {
return font( "fonts/SourceSansPro-Bold.otf" );
}
Font get(final int style, final int size) {
if (!registered)
register();
public Font exoBold() {
return font( "fonts/Exo2.0-Bold.otf" );
}
return new Font( fontName, style, size );
}
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)
private void register() {
try {
fontsByResourceName.put( fontResourceName, new SoftReference<>(
font = Font.createFont( Font.TRUETYPE_FONT, Resources.getResource( fontResourceName ).openStream() ) ) );
Font font = Font.createFont( Font.TRUETYPE_FONT, Resources.getResource( resourceName ).openStream() );
GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont( font );
registered = true;
}
catch (final FontFormatException | IOException 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(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.*;
@@ -29,9 +29,9 @@ public class SwingExecutorService extends AbstractExecutorService {
@Override
public void shutdown() {
shutdown = true;
synchronized (pendingCommands) {
shutdown = true;
if (pendingCommands.isEmpty())
terminated.add( true );
}
@@ -49,7 +49,9 @@ public class SwingExecutorService extends AbstractExecutorService {
@Override
public boolean isShutdown() {
return shutdown;
synchronized (pendingCommands) {
return shutdown;
}
}
@Override
@@ -65,10 +67,10 @@ public class SwingExecutorService extends AbstractExecutorService {
@Override
public void execute(@NotNull final Runnable command) {
if (shutdown)
throw new RejectedExecutionException( "Executor is shut down." );
synchronized (pendingCommands) {
if (shutdown)
throw new RejectedExecutionException( "Executor is shut down." );
pendingCommands.add( command );
}

View File

@@ -109,13 +109,14 @@ public class UnsignedIntegerModel extends SpinnerNumberModel implements Selectab
}
@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) {
removeChangeListener( changeListener );
changeListener = null;
}
setValue( selectedItem );
setValue( (selectedItem != null)? selectedItem: getMinimum() );
return selection( selectionConsumer );
}

View File

@@ -17,24 +17,47 @@ public class ApplePlatform implements IPlatform {
private static final Application application = Preconditions.checkNotNull(
Application.getApplication(), "Not an Apple Java application." );
private AppForegroundListener appForegroundHandler;
private AppReOpenedListener appReopenHandler;
@Override
public boolean installAppForegroundHandler(final Runnable handler) {
application.addAppEventListener( new AppForegroundListener() {
@Override
public void appMovedToBackground(final AppEvent.AppForegroundEvent e) {
}
if (appForegroundHandler == null)
application.addAppEventListener( appForegroundHandler = new AppForegroundListener() {
@Override
public void appMovedToBackground(final AppEvent.AppForegroundEvent e) {
}
@Override
public void appRaisedToForeground(final AppEvent.AppForegroundEvent e) {
handler.run();
}
} );
@Override
public void appRaisedToForeground(final AppEvent.AppForegroundEvent e) {
handler.run();
}
} );
return true;
}
@Override
public boolean removeAppForegroundHandler() {
if (appForegroundHandler == null)
return false;
application.removeAppEventListener( appForegroundHandler );
return true;
}
@Override
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;
}

View File

@@ -14,11 +14,21 @@ public class BasePlatform implements IPlatform {
return false;
}
@Override
public boolean removeAppForegroundHandler() {
return false;
}
@Override
public boolean installAppReopenHandler(final Runnable handler) {
return false;
}
@Override
public boolean removeAppReopenHandler() {
return false;
}
@Override
public boolean requestForeground() {
return false;

View File

@@ -12,8 +12,12 @@ public interface IPlatform {
boolean installAppForegroundHandler(Runnable handler);
boolean removeAppForegroundHandler();
boolean installAppReopenHandler(Runnable handler);
boolean removeAppReopenHandler();
boolean requestForeground();
boolean show(File file);

View File

@@ -17,24 +17,49 @@ 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
public boolean installAppForegroundHandler(final Runnable handler) {
desktop.addAppEventListener( new AppForegroundListener() {
@Override
public void appRaisedToForeground(final AppForegroundEvent e) {
handler.run();
}
if (appForegroundHandler == null)
desktop.addAppEventListener( appForegroundHandler = new AppForegroundListener() {
@Override
public void appRaisedToForeground(final AppForegroundEvent e) {
handler.run();
}
@Override
public void appMovedToBackground(final AppForegroundEvent e) {
}
} );
@Override
public void appMovedToBackground(final AppForegroundEvent e) {
}
} );
return true;
}
@Override
public boolean removeAppForegroundHandler() {
if (appForegroundHandler == null)
return false;
desktop.removeAppEventListener( appForegroundHandler );
return true;
}
@Override
public boolean installAppReopenHandler(final Runnable handler) {
desktop.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;
}

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

View File

@@ -1,14 +1,12 @@
package com.lyndir.masterpassword.gui.view;
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.Res;
import com.lyndir.masterpassword.model.MPUser;
import com.lyndir.masterpassword.model.impl.MPFileUserManager;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import javax.annotation.Nullable;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.BevelBorder;
@@ -21,28 +19,27 @@ public class MasterPasswordFrame extends JFrame {
private static final Logger logger = Logger.get( MasterPasswordFrame.class );
@SuppressWarnings("FieldCanBeLocal")
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();
private final UserContentPanel userContent;
@SuppressWarnings("MagicNumber")
public MasterPasswordFrame() {
super( "Master Password" );
setContentPane( root );
root.add( filesPanel );
root.add( Components.strut() );
root.add( userPanel );
JPanel root, userPanel;
setContentPane( root = Components.borderPanel( Res.colors().frameBg(), BoxLayout.PAGE_AXIS ) );
root.add( new FilesPanel() );
root.add( Components.strut() );
root.add( userPanel = Components.panel( new BorderLayout( 0, 0 ) ) );
userPanel.add( userContent.getUserToolbar(), BorderLayout.LINE_START );
userPanel.add( userContent.getSiteToolbar(), BorderLayout.LINE_END );
userPanel.add( Components.borderPanel(
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() );
addWindowListener( new WindowHandler() );
setPreferredSize( new Dimension( 800, 560 ) );
setDefaultCloseOperation( DISPOSE_ON_CLOSE );
pack();
@@ -59,4 +56,14 @@ public class MasterPasswordFrame extends JFrame {
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,13 +5,12 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.base.*;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.UnsignedInteger;
import com.google.common.util.concurrent.ListenableFuture;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ObjectUtils;
import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.gui.MPGuiConstants;
import com.lyndir.masterpassword.gui.MasterPassword;
import com.lyndir.masterpassword.gui.model.MPIncognitoUser;
import com.lyndir.masterpassword.gui.model.MPNewSite;
import com.lyndir.masterpassword.gui.*;
import com.lyndir.masterpassword.gui.model.*;
import com.lyndir.masterpassword.gui.util.*;
import com.lyndir.masterpassword.gui.util.Platform;
import com.lyndir.masterpassword.model.*;
@@ -28,12 +27,13 @@ import java.util.*;
import java.util.Optional;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.PlainDocument;
/**
@@ -42,9 +42,12 @@ import javax.swing.event.DocumentListener;
@SuppressWarnings("SerializableStoresNonSerializable")
public class UserContentPanel extends JPanel implements MasterPassword.Listener, MPUser.Listener {
private static final Random random = new Random();
private static final Logger logger = Logger.get( UserContentPanel.class );
private static final JButton iconButton = Components.button( Res.icons().user(), null, null );
private static final Random random = new Random();
private static final int SIZE_RESULT = 48;
private static final Logger logger = Logger.get( UserContentPanel.class );
private static final JButton iconButton = Components.button( Res.icons().user(), null, null );
private static final KeyStroke copyLoginKeyStroke = KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK );
private static final Pattern EACH_CHARACTER = Pattern.compile( "." );
private final JButton addButton = Components.button( Res.icons().add(), event -> addUser(),
"Add a new user to Master Password." );
@@ -290,9 +293,9 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
private final JButton resetButton = Components.button( Res.icons().reset(), event -> resetUser(),
"Change the master password for this user." );
private final JPasswordField masterPasswordField = Components.passwordField();
private final JLabel errorLabel = Components.label();
private final JLabel identiconLabel = Components.label( SwingConstants.CENTER );
private final JPasswordField masterPasswordField;
private final JLabel errorLabel;
private final JLabel identiconLabel;
private Future<?> identiconJob;
@@ -312,16 +315,16 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
add( Components.strut() );
add( identiconLabel );
add( identiconLabel = Components.label( SwingConstants.CENTER ) );
identiconLabel.setFont( Res.fonts().emoticonsFont( Components.TEXT_SIZE_CONTROL ) );
add( Box.createGlue() );
add( Components.label( "Master Password:" ) );
add( Components.strut() );
add( masterPasswordField );
add( masterPasswordField = Components.passwordField() );
masterPasswordField.addActionListener( this );
masterPasswordField.getDocument().addDocumentListener( this );
add( errorLabel );
add( errorLabel = Components.label() );
errorLabel.setForeground( Res.colors().errorFg() );
add( Box.createGlue() );
}
@@ -403,6 +406,9 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
Res.job( () -> {
try {
user.authenticate( masterPassword );
if (user instanceof MPFileUser)
((MPFileUser) user).migrateTo( MPMarshalFormat.DEFAULT );
}
catch (final MPIncorrectMasterPasswordException e) {
logger.wrn( e, "During user authentication for: %s", user );
@@ -458,9 +464,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
}
private final class AuthenticatedUserPanel extends JPanel implements KeyListener, MPUser.Listener {
public static final int SIZE_RESULT = 48;
private final class AuthenticatedUserPanel extends JPanel implements KeyListener, MPUser.Listener, KeyEventDispatcher {
private final JButton userButton = Components.button( Res.icons().user(), event -> showUserPreferences(),
"Show user preferences." );
@@ -468,22 +472,28 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
"Sign out and lock user." );
private final JButton settingsButton = Components.button( Res.icons().settings(), event -> showSiteSettings(),
"Show site settings." );
private final JButton questionsButton = Components.button( Res.icons().question(), null,
private final JButton questionsButton = Components.button( Res.icons().question(), event -> showSiteQuestions(),
"Show site recovery questions." );
private final JButton editButton = Components.button( Res.icons().edit(), event -> showSiteValues(),
"Set/save personal password/login." );
private final JButton keyButton = Components.button( Res.icons().key(), event -> showSiteKeys(),
"Cryptographic site keys." );
private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteSite(),
"Delete the site from the user." );
@Nonnull
private final MPUser<?> user;
private final JLabel passwordLabel = Components.label( SwingConstants.CENTER );
private final JLabel passwordField = Components.heading( SwingConstants.CENTER );
private final JLabel queryLabel = Components.label();
private final JTextField queryField = Components.textField( null, this::updateSites );
private final CollectionListModel<MPSite<?>> sitesModel =
new CollectionListModel<MPSite<?>>().selection( this::showSiteResult );
private final JList<MPSite<?>> sitesList =
Components.list( sitesModel, this::getSiteDescription );
private final MPUser<?> user;
private final JLabel resultLabel;
private final JLabel resultField;
private final JLabel answerLabel;
private final JLabel answerField;
private final JLabel queryLabel;
private final JTextField queryField;
private final CollectionListModel<MPQuery.Result<? extends MPSite<?>>> sitesModel;
private final CollectionListModel<MPQuery.Result<? extends MPQuestion>> questionsModel;
private final JList<MPQuery.Result<? extends MPSite<?>>> sitesList;
private boolean showLogin;
private Future<?> updateSitesJob;
private AuthenticatedUserPanel(@Nonnull final MPUser<?> user) {
@@ -499,53 +509,92 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
siteToolbar.add( settingsButton );
siteToolbar.add( questionsButton );
siteToolbar.add( editButton );
siteToolbar.add( keyButton );
siteToolbar.add( deleteButton );
settingsButton.setEnabled( false );
questionsButton.setEnabled( false );
editButton.setEnabled( false );
keyButton.setEnabled( false );
deleteButton.setEnabled( false );
answerLabel = Components.label( "Answer:" );
answerField = Components.heading( SwingConstants.CENTER );
answerField.setForeground( Res.colors().highlightFg() );
answerField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
questionsModel = new CollectionListModel<MPQuery.Result<? extends MPQuestion>>().selection( this::showQuestionItem );
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
add( passwordLabel );
add( passwordField );
passwordField.setForeground( Res.colors().highlightFg() );
passwordField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
add( resultLabel = Components.label( SwingConstants.CENTER ) );
add( resultField = Components.heading( SwingConstants.CENTER ) );
resultField.setForeground( Res.colors().highlightFg() );
resultField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
add( Box.createGlue() );
add( Components.strut() );
add( queryLabel );
add( queryLabel = Components.label() );
queryLabel.setText( strf( "%s's password for:", user.getFullName() ) );
add( queryField );
add( queryField = Components.textField( null, this::updateSites ) );
queryField.putClientProperty( "JTextField.variant", "search" );
queryField.addActionListener( event -> useSite() );
queryField.addActionListener( this::useSite );
queryField.getInputMap().put( copyLoginKeyStroke, JTextField.notifyAction );
queryField.addKeyListener( this );
queryField.requestFocusInWindow();
add( Components.strut() );
add( Components.scrollPane( sitesList ) );
sitesModel.registerList( sitesList );
add( Box.createGlue() );
add( Components.scrollPane( sitesList = Components.list(
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 -> {
if (null != SwingUtilities.windowForComponent( this ))
user.addListener( this );
else
user.removeListener( this );
if (HierarchyEvent.DISPLAYABILITY_CHANGED == (e.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED)) {
if (null != SwingUtilities.windowForComponent( this )) {
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher( this );
user.addListener( this );
} else {
KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher( this );
user.removeListener( this );
}
}
} );
}
public void showUserPreferences() {
ImmutableList.Builder<Component> components = ImmutableList.builder();
MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null;
if (fileUser != null)
components.add( Components.label( "Default Password Type:" ),
Components.comboBox( MPResultType.values(), MPResultType::getLongName,
fileUser.getDefaultType(), fileUser::setDefaultType ),
Components.strut() );
components.add( Components.label( "Default Algorithm:" ),
Components.comboBox( MPAlgorithm.Version.values(), MPAlgorithm.Version::name,
user.getAlgorithm().version(),
version -> user.setAlgorithm( version.getAlgorithm() ) ) );
user.getAlgorithm().version(), 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(
BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) );
@@ -556,7 +605,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
}
public void showSiteSettings() {
MPSite<?> site = sitesModel.getSelectedItem();
MPSite<?> site = getSite();
if (site == null)
return;
@@ -572,12 +621,14 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
Components.strut() );
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 ),
Components.strut() );
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 ),
Components.strut() );
@@ -587,12 +638,183 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
Components.textField( fileSite.getUrl(), fileSite::setUrl ),
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] ) ) ) );
}
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() {
MPSite<?> site = sitesModel.getSelectedItem();
MPSite<?> site = getSite();
if (site == null)
return;
@@ -602,96 +824,193 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
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)
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();
try {
MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null;
if (fileSite != null)
parameters.add( Res.format( fileSite.getLastUsed() ) );
parameters.add( site.getAlgorithm().version() );
parameters.add( strf( "#%d", site.getCounter().longValue() ) );
parameters.add( strf( "<em>%s</em>", site.getLogin() ) );
if ((fileSite != null) && (fileSite.getUrl() != null))
parameters.add( fileSite.getUrl() );
}
catch (final MPAlgorithmException | MPKeyUnavailableException e) {
logger.err( e, "While generating site description." );
parameters.add( e.getLocalizedMessage() );
}
MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null;
if (fileSite != null)
parameters.add( Res.format( fileSite.getLastUsed() ) );
parameters.add( site.getAlgorithm().version() );
parameters.add( strf( "#%d", site.getCounter().longValue() ) );
if ((fileSite != null) && (fileSite.getUrl() != null))
parameters.add( fileSite.getUrl() );
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() ) );
}
private void useSite() {
MPSite<?> site = sitesModel.getSelectedItem();
private String getQuestionDescription(@Nullable final MPQuery.Result<? extends MPQuestion> item) {
MPQuestion question = (item != null)? item.getValue(): null;
if (question == null)
return "<site>";
if (question instanceof MPNewQuestion)
return strf( "<html>%s &lt;Add new question&gt;</html>", item.getKeyAsHTML() );
return strf( "<html>%s</html>", item.getKeyAsHTML() );
}
private void useSite(final ActionEvent event) {
MPSite<?> site = getSite();
if (site instanceof MPNewSite) {
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
if (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(
this, strf( "<html>Remember the site <strong>%s</strong>?</html>", site.getSiteName() ),
"New Site", JOptionPane.YES_NO_OPTION )) {
sitesModel.setSelectedItem( user.addSite( site.getSiteName() ) );
useSite();
}
return;
"New Site", JOptionPane.YES_NO_OPTION ))
return;
site = ((MPNewSite) site).addTo( user );
}
showSiteResult( site, result -> {
boolean loginResult = (copyLoginKeyStroke.getModifiers() & event.getModifiers()) != 0;
MPSite<?> fsite = site;
Res.ui( getSiteResult( site, loginResult ), result -> {
if (result == null)
return;
if (site instanceof MPFileSite)
((MPFileSite) site).use();
if (fsite instanceof MPFileSite)
((MPFileSite) fsite).use();
Transferable clipboardContents = new StringSelection( result );
Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null );
Res.ui( () -> {
Window window = SwingUtilities.windowForComponent( UserContentPanel.this );
if (window instanceof Frame)
((Frame) window).setExtendedState( Frame.ICONIFIED );
} );
copyResult( result );
} );
}
private void showSiteResult(@Nullable final MPSite<?> site) {
showSiteResult( site, null );
private void setShowLogin(final boolean showLogin) {
if (showLogin == this.showLogin)
return;
this.showLogin = showLogin;
showSiteItem( sitesModel.getSelectedItem() );
}
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;
}
private void showSiteItem(@Nullable final MPQuery.Result<? extends MPSite<?>> item) {
MPSite<?> site = (item != null)? item.getValue(): null;
Res.ui( getSiteResult( site, showLogin ), result -> {
if (!showLogin && (site != null))
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.job( () -> {
if ((result == null) || result.isEmpty())
resultField.setText( " " );
else if (!showLogin && user.getPreferences().isHidePasswords())
resultField.setText( EACH_CHARACTER.matcher( result ).replaceAll( "" ) );
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 {
String result = site.getResult();
if (resultCallback != null)
resultCallback.accept( result );
Res.ui( () -> {
passwordLabel.setText( strf( "Your password for %s:", site.getSiteName() ) );
passwordField.setText( result );
settingsButton.setEnabled( true );
deleteButton.setEnabled( true );
} );
if (site != null)
return loginResult? site.getLogin(): site.getResult();
}
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
logger.err( e, "While resolving password for: %s", site );
}
return null;
} );
}
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
public void keyTyped(final KeyEvent event) {
}
@@ -708,27 +1027,33 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
sitesList.dispatchEvent( event );
}
private synchronized void updateSites(@Nullable final String query) {
private synchronized void updateSites(@Nullable final String queryText) {
if (updateSitesJob != null)
updateSitesJob.cancel( true );
updateSitesJob = Res.job( () -> {
Collection<MPSite<?>> sites = new LinkedList<>();
if (!Strings.isNullOrEmpty( query )) {
sites.addAll( new LinkedList<>( user.findSites( query ) ) );
MPQuery query = new MPQuery( queryText );
Collection<MPQuery.Result<? extends MPSite<?>>> siteItems = new LinkedList<MPQuery.Result<? extends MPSite<?>>>(
query.find( user.getSites(), MPSite::getSiteName ) );
if (sites.stream().noneMatch( site -> site.getSiteName().equalsIgnoreCase( query ) ))
sites.add( new MPNewSite( user, query ) );
}
if (!Strings.isNullOrEmpty( queryText ))
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
public void onUserUpdated(final MPUser<?> user) {
updateSites( queryField.getText() );
showSiteResult( sitesModel.getSelectedItem() );
showSiteItem( sitesModel.getSelectedItem() );
}
@Override
@@ -738,5 +1063,13 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
@Override
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: 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

@@ -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);
@Nonnull
@Nullable
default String getAnswer()
throws MPKeyUnavailableException, MPAlgorithmException {
return getAnswer( null );
}
@Nullable
String getAnswer(@Nullable String state)
throws MPKeyUnavailableException, MPAlgorithmException;

View File

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

View File

@@ -18,7 +18,6 @@
package com.lyndir.masterpassword.model;
import com.google.common.collect.ImmutableCollection;
import com.lyndir.masterpassword.*;
import java.util.Collection;
import javax.annotation.Nonnull;
@@ -39,6 +38,9 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
@Nonnull
String getFullName();
@Nonnull
MPUserPreferences getPreferences();
// - Algorithm
@Nonnull
@@ -46,11 +48,6 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
void setAlgorithm(MPAlgorithm algorithm);
@Nullable
default MPResultType getDefaultType() {
return null;
}
@Nullable
byte[] getKeyID();
@@ -100,6 +97,7 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
// - Relations
@Nonnull
S addSite(String siteName);
@Nonnull
@@ -110,12 +108,9 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
@Nonnull
Collection<S> getSites();
@Nonnull
ImmutableCollection<S> findSites(@Nullable String query);
void addListener(Listener listener);
boolean addListener(Listener listener);
boolean removeListener(Listener listener);
void removeListener(Listener 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
*/
public class Changeable {
public abstract class Changeable {
private static final ExecutorService changeExecutor = Executors.newSingleThreadExecutor();
@@ -15,7 +15,9 @@ public class Changeable {
private Grouping grouping = Grouping.APPLY;
private boolean changed;
void setChanged() {
protected abstract void onChanged();
public void setChanged() {
synchronized (mutex) {
if (changed)
return;
@@ -37,9 +39,6 @@ public class Changeable {
} );
}
protected void onChanged() {
}
public void beginChanges() {
synchronized (mutex) {
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.model.MPQuestion;
import com.lyndir.masterpassword.model.MPSite;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -33,14 +34,23 @@ import org.jetbrains.annotations.NotNull;
*/
public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
private final String keyword;
private MPResultType type;
private final MPSite<?> site;
private final String keyword;
protected MPBasicQuestion(final String keyword, final MPResultType type) {
private MPResultType type;
protected MPBasicQuestion(final MPSite<?> site, final String keyword, final MPResultType type) {
this.site = site;
this.keyword = keyword;
this.type = type;
}
@Nonnull
@Override
public MPSite<?> getSite() {
return site;
}
@Nonnull
@Override
public String getKeyword() {
@@ -55,14 +65,14 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
@Override
public void setType(final MPResultType type) {
if (Objects.equals(this.type, type))
if (this.type == type)
return;
this.type = type;
setChanged();
}
@Nonnull
@Nullable
@Override
public String getAnswer(@Nullable final String state)
throws MPKeyUnavailableException, MPAlgorithmException {
@@ -70,15 +80,10 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
return getSite().getResult( MPKeyPurpose.Recovery, getKeyword(), null, getType(), state );
}
@Nonnull
@Override
public abstract MPBasicSite<?, ?> getSite();
@Override
protected void onChanged() {
super.onChanged();
getSite().setChanged();
if (site instanceof Changeable)
((Changeable) site).setChanged();
}
@Override

View File

@@ -35,9 +35,9 @@ import javax.annotation.Nullable;
public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> extends Changeable
implements MPSite<Q> {
private final Collection<Q> questions = new LinkedHashSet<>();
private final U user;
private final String siteName;
private final Collection<Q> questions = new TreeSet<>();
private MPAlgorithm algorithm;
private UnsignedInteger counter;
@@ -55,17 +55,20 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
this.siteName = siteName;
this.algorithm = (algorithm != null)? algorithm: this.user.getAlgorithm();
this.counter = (counter != null)? counter: this.algorithm.mpw_default_counter();
this.resultType = (resultType != null)? resultType:
ifNotNullElse( this.user.getDefaultType(), this.algorithm.mpw_default_result_type() );
this.resultType = (resultType != null)? resultType: this.user.getPreferences().getDefaultType();
this.loginType = (loginType != null)? loginType: this.algorithm.mpw_default_login_type();
}
// - Meta
@Nonnull
@Override
public String getSiteName() {
return siteName;
}
// - Algorithm
@Nonnull
@Override
public MPAlgorithm getAlgorithm() {
@@ -104,7 +107,7 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
@Override
public void setResultType(final MPResultType resultType) {
if (Objects.equals( this.resultType, resultType ))
if (this.resultType == resultType)
return;
this.resultType = resultType;
@@ -119,14 +122,14 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
@Override
public void setLoginType(@Nullable final MPResultType loginType) {
if (Objects.equals( this.loginType, loginType ))
if (this.loginType == loginType)
return;
this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() );
setChanged();
}
@Nonnull
@Nullable
@Override
public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nullable final String state)
throws MPKeyUnavailableException, MPAlgorithmException {
@@ -134,8 +137,10 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
return getResult( keyPurpose, keyContext, getCounter(), getResultType(), state );
}
protected String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
@Nullable final UnsignedInteger counter, final MPResultType type, @Nullable final String state)
@Nullable
@Override
public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
@Nullable final UnsignedInteger counter, final MPResultType type, @Nullable final String state)
throws MPKeyUnavailableException, MPAlgorithmException {
return getUser().getMasterKey().siteResult(
@@ -143,8 +148,10 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
keyPurpose, keyContext, type, state );
}
protected String getState(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
@Nullable final UnsignedInteger counter, final MPResultType type, final String state)
@Nonnull
@Override
public String getState(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
@Nullable final UnsignedInteger counter, final MPResultType type, final String state)
throws MPKeyUnavailableException, MPAlgorithmException {
return getUser().getMasterKey().siteState(
@@ -152,7 +159,7 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
keyPurpose, keyContext, type, state );
}
@Nonnull
@Nullable
@Override
public String getLogin(@Nullable final String state)
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 );
}
// - Relations
@Nonnull
@Override
public boolean addQuestion(final Q question) {
if (!questions.add( question ))
return false;
public U getUser() {
return user;
}
@Nonnull
@Override
public Q addQuestion(final Q question) {
questions.add( question );
setChanged();
return true;
return question;
}
@Override
@@ -184,16 +199,8 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
return Collections.unmodifiableCollection( questions );
}
@Nonnull
@Override
public U getUser() {
return user;
}
@Override
protected void onChanged() {
super.onChanged();
if (user instanceof Changeable)
((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 com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableSortedSet;
import com.lyndir.lhunath.opal.system.CodeUtils;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.*;
@@ -47,7 +45,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
@Nullable
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) {
this( 0, fullName, algorithm );
@@ -79,6 +77,12 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
return fullName;
}
@Nonnull
@Override
public MPUserPreferences getPreferences() {
return new MPBasicUserPreferences<MPBasicUser<?>>( this );
}
@Nonnull
@Override
public MPAlgorithm getAlgorithm() {
@@ -177,7 +181,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
@Nonnull
@Override
public S addSite(final S site) {
sites.put( site.getSiteName(), site );
sites.add( site );
setChanged();
return site;
@@ -185,7 +189,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
@Override
public boolean deleteSite(final MPSite<?> site) {
if (!sites.values().remove( site ))
if (!sites.remove( site ))
return false;
setChanged();
@@ -195,35 +199,21 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
@Nonnull
@Override
public Collection<S> getSites() {
return Collections.unmodifiableCollection( sites.values() );
}
@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();
return Collections.unmodifiableCollection( sites );
}
@Override
public boolean addListener(final Listener listener) {
return listeners.add( listener );
public void addListener(final Listener listener) {
listeners.add( listener );
}
@Override
public boolean removeListener(final Listener listener) {
return listeners.remove( listener );
public void removeListener(final Listener listener) {
listeners.remove( listener );
}
@Override
protected void onChanged() {
super.onChanged();
for (final Listener listener : listeners)
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 com.lyndir.masterpassword.*;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -30,16 +29,14 @@ import javax.annotation.Nullable;
*/
public class MPFileQuestion extends MPBasicQuestion {
private final MPFileSite site;
@Nullable
private String answerState;
@SuppressWarnings("TypeMayBeWeakened")
public MPFileQuestion(final MPFileSite site, final String keyword,
@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;
}
@@ -48,6 +45,8 @@ public class MPFileQuestion extends MPBasicQuestion {
return answerState;
}
@Nullable
@Override
public String getAnswer()
throws MPKeyUnavailableException, MPAlgorithmException {
return getAnswer( answerState );
@@ -66,9 +65,8 @@ public class MPFileQuestion extends MPBasicQuestion {
setChanged();
}
@Nonnull
@Override
public MPFileSite getSite() {
return site;
public void use() {
if (getSite() instanceof MPFileSite)
((MPFileSite) getSite()).use();
}
}

View File

@@ -93,7 +93,7 @@ public class MPFileSite extends MPBasicSite<MPFileUser, MPFileQuestion> {
setChanged();
}
@Nonnull
@Nullable
@Override
public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext)
throws MPKeyUnavailableException, MPAlgorithmException {
@@ -101,7 +101,7 @@ public class MPFileSite extends MPBasicSite<MPFileUser, MPFileQuestion> {
return getResult( keyPurpose, keyContext, getResultState() );
}
@Nonnull
@Nullable
@Override
public String getLogin()
throws MPKeyUnavailableException, MPAlgorithmException {
@@ -145,6 +145,12 @@ public class MPFileSite extends MPBasicSite<MPFileUser, MPFileQuestion> {
setChanged();
}
@Nonnull
@Override
public MPFileQuestion addQuestion(final String keyword) {
return addQuestion( new MPFileQuestion( this, keyword, null, null ) );
}
@Override
public int compareTo(@Nonnull final MPSite<?> o) {
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.masterpassword.*;
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
import com.lyndir.masterpassword.model.MPUser;
import com.lyndir.masterpassword.model.*;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.joda.time.Instant;
@@ -41,44 +39,48 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
@Nullable
private byte[] keyID;
private File path;
private File file;
private MPMarshalFormat format;
private MPMarshaller.ContentMode contentMode;
private ReadableInstant lastUsed;
private boolean complete;
private MPResultType defaultType;
private ReadableInstant lastUsed;
private boolean complete;
private final MPFileUserPreferences preferences;
@Nullable
public static MPFileUser load(final File file)
throws IOException, MPMarshalException {
for (final MPMarshalFormat format : MPMarshalFormat.values())
if (file.getName().endsWith( format.fileSuffix() ))
return format.unmarshaller().readUser( file );
if (format.matches( file ))
return format.unmarshaller().readUser( file );
return null;
}
public MPFileUser(final String fullName, final File path) {
this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm(), path );
public MPFileUser(final String fullName, final File location) {
this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm(), location );
}
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final File path) {
this( fullName, keyID, algorithm, 0, null, new Instant(),
MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, path );
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final File location) {
this( fullName, keyID, algorithm, 0, null, new Instant(), false,
MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, location );
}
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm,
final int avatar, @Nullable final MPResultType defaultType, final ReadableInstant lastUsed,
final MPMarshaller.ContentMode contentMode, final MPMarshalFormat format, final File path) {
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final int avatar,
@Nullable final MPResultType defaultType, final ReadableInstant lastUsed, final boolean hidePasswords,
final MPMarshaller.ContentMode contentMode, final MPMarshalFormat format, final File location) {
super( avatar, fullName, algorithm );
this.keyID = (keyID != null)? keyID.clone(): null;
this.defaultType = (defaultType != null)? defaultType: algorithm.mpw_default_result_type();
this.lastUsed = lastUsed;
this.path = path;
this.preferences = new MPFileUserPreferences( this, defaultType, hidePasswords );
this.format = format;
this.contentMode = contentMode;
if (location.isDirectory())
this.file = new File( location, getFullName() + getFormat().fileSuffix() );
else
this.file = location;
}
@Nullable
@@ -87,8 +89,10 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
return (keyID == null)? null: keyID.clone();
}
public void setPath(final File path) {
this.path = path;
@Nonnull
@Override
public MPUserPreferences getPreferences() {
return preferences;
}
@Override
@@ -115,39 +119,18 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
return format;
}
public void setFormat(final MPMarshalFormat format) {
if (Objects.equals( this.format, format ))
return;
this.format = format;
setChanged();
}
public MPMarshaller.ContentMode getContentMode() {
return contentMode;
}
public void setContentMode(final MPMarshaller.ContentMode contentMode) {
if (Objects.equals( this.contentMode, contentMode ))
if (this.contentMode == contentMode)
return;
this.contentMode = contentMode;
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() {
return lastUsed;
}
@@ -166,7 +149,47 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
}
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
@@ -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 {
if (isComplete())
getFormat().marshaller().marshall( this );
getFormat().marshaller().marshall( this );
return true;
}
catch (final MPKeyUnavailableException e) {
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) {
logger.err( e, "Unable to write out changes for user: %s", this );
}
return false;
}
@Override
@@ -207,6 +238,7 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
super.reset();
}
@Nonnull
@Override
public MPFileSite addSite(final String siteName) {
return addSite( new MPFileSite( this, siteName ) );

View File

@@ -18,11 +18,9 @@
package com.lyndir.masterpassword.model.impl;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import com.google.common.collect.ImmutableSortedSet;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.model.MPModelConstants;
import com.lyndir.masterpassword.model.MPConfig;
import java.io.File;
import java.io.IOException;
import java.util.*;
@@ -38,19 +36,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
public class MPFileUserManager {
@SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MPFileUserManager.class );
private static final MPFileUserManager instance;
static {
String rcDir = System.getenv( MPModelConstants.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 static final Logger logger = Logger.get( MPFileUserManager.class );
private static final MPFileUserManager instance = create( MPConfig.get().rcDir() );
private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
private final Map<String, MPFileUser> userByName = new HashMap<>();
@@ -66,6 +53,7 @@ public class MPFileUserManager {
protected MPFileUserManager(final File path) {
this.path = path;
reload();
}
public void reload() {
@@ -77,18 +65,17 @@ public class MPFileUserManager {
return;
}
for (final File file : pathFiles)
try {
MPFileUser user = MPFileUser.load( file );
if (user != null) {
MPFileUser previousUser = userByName.put( user.getFullName(), user );
if ((previousUser != null) && (previousUser.getFormat().ordinal() > user.getFormat().ordinal()))
userByName.put( previousUser.getFullName(), previousUser );
}
}
catch (final IOException | MPMarshalException e) {
logger.err( e, "Couldn't read user from: %s", file );
}
for (final MPMarshalFormat format : MPMarshalFormat.values())
for (final File file : pathFiles)
if (format.matches( file ))
try {
MPFileUser user = MPFileUser.load( file );
if (user != null)
add( user );
}
catch (final IOException | MPMarshalException e) {
logger.err( e, "Couldn't read user from: %s", file );
}
fireUpdated();
}
@@ -98,12 +85,19 @@ public class MPFileUserManager {
}
public MPFileUser add(final MPFileUser user) {
user.setPath( getPath() );
user.save();
// 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.migrateTo( getPath() );
user.migrateTo( MPMarshalFormat.DEFAULT );
MPFileUser oldUser = userByName.put( user.getFullName(), user );
if (oldUser != null)
if (oldUser != null) {
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();
return user;
@@ -128,12 +122,13 @@ public class MPFileUserManager {
return ImmutableSortedSet.copyOf( userByName.values() );
}
public boolean addListener(final Listener listener) {
return listeners.add( listener );
public void addListener(final Listener listener) {
if (listeners.add( listener ))
listener.onFilesUpdated( getFiles() );
}
public boolean removeListener(final Listener listener) {
return listeners.remove( listener );
public void removeListener(final Listener listener) {
listeners.remove( listener );
}
private void fireUpdated() {
@@ -141,7 +136,6 @@ public class MPFileUserManager {
return;
ImmutableSortedSet<MPFileUser> files = getFiles();
for (final Listener listener : listeners)
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

@@ -56,7 +56,7 @@ public class MPFlatMarshaller implements MPMarshaller {
content.append( "# Avatar: " ).append( user.getAvatar() ).append( '\n' );
content.append( "# Key ID: " ).append( user.exportKeyID() ).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( "##\n" );
content.append( "#\n" );

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

View File

@@ -19,6 +19,9 @@
package com.lyndir.masterpassword.model.impl;
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 java.util.*;
@@ -27,31 +30,59 @@ import java.util.*;
* @author lhunath, 2018-05-14
*/
@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
final Map<String, Object> any = new LinkedHashMap<>();
@JsonAnyGetter
public Map<String, Object> getAny() {
public Map<String, Object> any() {
return Collections.unmodifiableMap( any );
}
@SuppressWarnings("unchecked")
public <V> V any(final String key) {
return (V) any.get( key );
}
@SuppressWarnings("EqualsAndHashcode")
public static class MPJSONEmptyValue {
@Override
@SuppressWarnings({ "ChainOfInstanceofChecks", "Contract" })
@SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
@SuppressFBWarnings({ "EQ_UNUSUAL", "EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS", "HE_EQUALS_USE_HASHCODE" })
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<?>)
return ((Collection<?>) obj).isEmpty();
if (obj instanceof Map<?, ?>)
return ((Map<?, ?>) obj).isEmpty();
if (obj instanceof MPJSONFile.Site.Ext)
return ((MPJSONAnyObject) obj).any.isEmpty();
if (obj instanceof MPJSONAnyObject)
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.lyndir.lhunath.opal.system.CodeUtils;
import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPModelConstants;
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
import com.lyndir.masterpassword.model.MPModelConstants;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.util.LinkedHashMap;
@@ -44,22 +44,6 @@ import org.joda.time.Instant;
@SuppressFBWarnings("URF_UNREAD_FIELD")
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() {
}
@@ -79,7 +63,12 @@ public class MPJSONFile extends MPJSONAnyObject {
user.last_used = MPModelConstants.dateTimeFormatter.print( modelUser.getLastUsed() );
user.key_id = modelUser.exportKeyID();
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"
sites = new LinkedHashMap<>();
@@ -130,8 +119,11 @@ public class MPJSONFile extends MPJSONAnyObject {
}
} );
site._ext_mpw = new Site.Ext();
site._ext_mpw.url = modelSite.getUrl();
site._ext_mpw = new Site.Ext() {
{
url = modelSite.getUrl();
}
};
}
}
@@ -140,10 +132,11 @@ public class MPJSONFile extends MPJSONAnyObject {
return new MPFileUser(
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)? 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,
MPMarshalFormat.JSON, file.getParentFile()
MPMarshalFormat.JSON, file
);
}
@@ -170,7 +163,7 @@ public class MPJSONFile extends MPJSONAnyObject {
if (fileSite.questions != null)
for (final Map.Entry<String, Site.Question> questionEntry : fileSite.questions.entrySet()) {
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 );
if (!export.redacted && (fileQuestion.answer != null))
@@ -209,8 +202,17 @@ public class MPJSONFile extends MPJSONAnyObject {
String key_id;
@Nullable
MPAlgorithm.Version algorithm;
@Nullable
MPResultType default_type;
Ext _ext_mpw;
public static class Ext extends MPJSONAnyObject {
@Nullable
MPResultType default_type;
boolean hide_passwords;
}
}

View File

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