2
0

Compare commits

...

41 Commits

Author SHA1 Message Date
Maarten Billemont
8f35ac5f64 Improved libxml2 cflags/ldflags. 2018-09-26 00:43:23 -04:00
Maarten Billemont
06100510c3 2.7-java-9 2018-09-26 00:03:36 -04:00
Maarten Billemont
1bf6109038 Hide passwords option & fix settings for new sites. 2018-09-26 00:00:05 -04:00
Maarten Billemont
f2fa2a25b2 Pass font resolution through the GraphicsEnvironment. 2018-09-25 20:55:04 -04:00
Maarten Billemont
2a0cfd3a32 Documentation and interface tweaks. 2018-09-22 19:43:13 -04:00
Maarten Billemont
3070967d34 Convenience flag for building a debuggable binary. 2018-09-22 14:22:17 -04:00
Maarten Billemont
e4837a284a Fall back to getline if ncurses cannot be initialized (eg. TERM not set). 2018-09-22 14:14:18 -04:00
Maarten Billemont
06ebe954f1 Clarify the interface a bit. 2018-09-22 14:13:00 -04:00
Maarten Billemont
48d4668575 Ensure all read utilities yield constant string pointers for safety. 2018-09-22 14:12:53 -04:00
Maarten Billemont
af768329a3 2.7-java-8 2018-09-13 22:38:45 -04:00
Maarten Billemont
9a04c28054 Fix initialization of text consumers & action handlers on sites list. 2018-09-13 22:37:28 -04:00
Maarten Billemont
ec9c55ec4d Access login names by holding shift. 2018-09-13 15:49:58 -04:00
Maarten Billemont
d8a735e1b1 Improve render speed of lists. 2018-09-13 15:49:42 -04:00
Maarten Billemont
a1eee88a54 Improved search query support. 2018-09-12 13:12:10 -04:00
Maarten Billemont
ac5286853a Write release JARs to site directory. 2018-08-28 02:43:24 -04:00
Maarten Billemont
39f6893742 2.7-java-7 2018-08-27 18:24:46 -04:00
Maarten Billemont
7bf7b8981c Fix duplication of user names in files list. 2018-08-27 18:24:00 -04:00
Maarten Billemont
09abe21fed Release masterpassword-gui-2.7.6 2018-08-27 13:22:03 -04:00
Maarten Billemont
6fae0fe425 2.7-java-6 2018-08-27 13:11:32 -04:00
Maarten Billemont
0558176847 Fix dependencies in native mpw DLLs. 2018-08-27 13:07:38 -04:00
Maarten Billemont
c553201cda Small site update. 2018-08-26 20:41:27 -04:00
Maarten Billemont
665be9494b Release masterpassword-gui-2.7.5 2018-08-26 18:19:01 -04:00
Maarten Billemont
5ca81b4aa7 Don't use daemon when setting release passwords in environment. 2018-08-25 11:57:36 -04:00
Maarten Billemont
3cbb063926 Refactor Native to try and load other architectures. 2018-08-24 16:33:14 -04:00
Maarten Billemont
d5551c8c8c Key calculator and access to the full algorithm. 2018-08-24 13:48:53 -04:00
Maarten Billemont
9a40e52d53 2.7-java-5 2018-08-19 16:13:39 -04:00
Maarten Billemont
6f0d768e69 Saving custom passwords and logins. 2018-08-19 16:11:43 -04:00
Maarten Billemont
40fdc8d248 Fix initialization dependency cycle & load files on init. 2018-08-18 13:43:41 -04:00
Maarten Billemont
6b9e1b8cb8 Standard label font & fix warnings. 2018-08-14 12:06:04 -04:00
Maarten Billemont
f41cdb8742 Site security questions and copy login name. 2018-08-13 17:53:31 -04:00
Maarten Billemont
10c6d203b8 Implement security answers & immediate site lookup. 2018-08-07 00:07:16 -04:00
Maarten Billemont
7d1aa9c9f4 Release 2.7.4 2018-08-02 12:34:24 -04:00
Maarten Billemont
c26281e3b7 2.7-java-4 2018-08-02 12:20:45 -04:00
Maarten Billemont
f0b1f0c9e0 Build for older glibc. 2018-08-02 12:19:49 -04:00
Maarten Billemont
9682efc7c9 Release masterpassword-gui-2.7.3 2018-08-02 01:44:50 -04:00
Maarten Billemont
1264cad377 2.7-java-3 2018-08-02 01:37:37 -04:00
Maarten Billemont
d185a0af14 Add mpw native binary for windows 32-bit. 2018-08-02 01:37:10 -04:00
Maarten Billemont
4275a6cc61 Fix build on Windows. 2018-08-02 01:32:55 -04:00
Maarten Billemont
c94ff429e8 Switch linux build of libmpw to debian for glibc instead of musl libc. 2018-08-01 20:13:42 -04:00
Maarten Billemont
00744cb264 Statically link the mpw library. 2018-08-01 14:20:47 -04:00
Maarten Billemont
7202fe6d1d Bump site for release of masterpassword-gui-2.7.2.jar 2018-07-31 15:35:57 -04:00
61 changed files with 1489 additions and 590 deletions

1
.gitignore vendored
View File

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

View File

@@ -1,9 +1,12 @@
FROM alpine FROM debian:stable-slim
# For i386 # For i386
#FROM i386/alpine #FROM i386/debian:stable-slim
#ENTRYPOINT ["linux32", "--"] #ENTRYPOINT ["linux32", "--"]
RUN apk add --no-cache git libtool automake autoconf make g++ bash openjdk8 # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199
RUN mkdir -p /usr/share/man/man1
RUN apt-get update && apt-get install -y default-jdk-headless git-core bash libtool automake autoconf make g++
RUN git clone --depth=3 $(: --shallow-submodules) --recurse-submodules https://gitlab.com/MasterPassword/MasterPassword.git /mpw RUN git clone --depth=3 $(: --shallow-submodules) --recurse-submodules https://gitlab.com/MasterPassword/MasterPassword.git /mpw
RUN cd /mpw/gradle && ./gradlew -i clean build RUN cd /mpw/gradle && ./gradlew -i clean build

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

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

View File

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

View File

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

View File

@@ -32,8 +32,15 @@ PATH+=:/usr/local/bin
needs() { _needs "$@"; } needs() { _needs "$@"; }
_needs() { _needs() {
local failed=0 local failed=0
for tool; do for spec; do
hash "$tool" || { echo >&2 "Missing: $tool. Please install this tool."; (( failed++ )); } IFS=: read pkg tools <<< "$spec"
IFS=, read -a tools <<< "${tools:-$pkg}"
for tool in "${tools[@]}"; do
hash "$tool" 2>/dev/null && continue 2
done
echo >&2 "Missing: $pkg. Please install this package."
(( failed++ ))
done done
return $failed return $failed
@@ -51,15 +58,15 @@ _initialize() {
# #
# Check if all tools needed for the default implementations are available. # Check if all tools needed for the default implementations are available.
# #
# By default, this will check for `libtool` (for libtoolize), `automake` (for aclocal) and `autoconf` (for autoreconf). # By default, this will check for `libtool` (for libtoolize), `automake` (for aclocal), `autoconf` (for autoreconf) and make.
initialize_needs() { _initialize_needs "$@"; } initialize_needs() { _initialize_needs "$@"; }
_initialize_needs() { _initialize_needs() {
if [[ $platform = windows ]]; then if [[ $platform = windows ]]; then
needs cmd needs cmd
export VSINSTALLDIR="${VSINSTALLDIR:-$(cd "$(cygpath -F 0x002a)/Microsoft Visual Studio"/*/*/Common7/.. && pwd)}" export VSINSTALLDIR="${VSINSTALLDIR:-$(cd "$(cygpath -F 0x002a)/Microsoft Visual Studio"/*/*/Common7/.. && pwd)}"
[[ -e "$VSINSTALLDIR/Common7/Tools/VsMSBuildCmd.bat" ]] || { echo >&2 "Missing: msbuild. Please install 'Build Tools for Visual Studio'."; return 1; } [[ -e "$VSINSTALLDIR/Common7/Tools/VsMSBuildCmd.bat" ]] || { echo >&2 "Missing: msbuild. Please install 'Build Tools for Visual Studio'. See https://visualstudio.microsoft.com/downloads/?q=build+tools"; return 1; }
else else
needs libtool automake autoconf needs libtool:libtoolize,glibtoolize automake autoconf make
fi fi
} }
@@ -195,7 +202,7 @@ _target_build() {
if [[ $platform = windows ]]; then if [[ $platform = windows ]]; then
# I cannot for the life of me figure out how to pass this command directly into cmd. # I cannot for the life of me figure out how to pass this command directly into cmd.
printf '"%%VSINSTALLDIR%%\Common7\Tools\VsMSBuildCmd.bat" && msbuild /t:Rebuild /p:Configuration=ReleaseDLL;Platform=%s;OutDir=%s' "$arch" "$(cygpath -w "${prefix##$PWD/}/$arch/")" > .build.bat printf '"%%VSINSTALLDIR%%\Common7\Tools\VsMSBuildCmd.bat" && msbuild /t:Rebuild /p:Configuration=Release;Platform=%s;OutDir=%s' "$arch" "$(cygpath -w "${prefix##$PWD/}/$arch/")" > .build.bat
cmd //c .build.bat cmd //c .build.bat
rm -f .build.bat rm -f .build.bat
else else
@@ -271,7 +278,10 @@ _finalize_merge() {
# By default, this will run `make clean`. # By default, this will run `make clean`.
finalize_clean() { _finalize_clean "$@"; } finalize_clean() { _finalize_clean "$@"; }
_finalize_clean() { _finalize_clean() {
[[ ! -e Makefile ]] || make -s clean if [[ $platform = windows ]]; then :
else
[[ ! -e Makefile ]] || make -s clean
fi
} }
# build <name> [<platform>] # build <name> [<platform>]

View File

@@ -1,4 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
source "${BASH_SOURCE%/*}/build_lib" source "${BASH_SOURCE%/*}/build_lib"
finalize_merge() {
local prefix=$1 platform=$2; shift 2
local archs=( "$@" )
cp -a "src/libsodium/include" "$prefix/out"
_finalize_merge "$prefix" "$platform" "${archs[@]}"
}
build libsodium windows build libsodium windows

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

@@ -7,7 +7,7 @@
// TODO: We may need to zero the jbytes safely. // TODO: We may need to zero the jbytes safely.
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env; JNIEnv* env;
if ((*vm)->GetEnv( vm, (void **)&env, JNI_VERSION_1_6 ) != JNI_OK) if ((*vm)->GetEnv( vm, (void **)&env, JNI_VERSION_1_6 ) != JNI_OK)
return -1; return -1;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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

@@ -26,11 +26,11 @@ import com.lyndir.masterpassword.model.MPModelConstants;
* @author lhunath, 2014-08-31 * @author lhunath, 2014-08-31
*/ */
@SuppressWarnings("CallToSystemGetenv") @SuppressWarnings("CallToSystemGetenv")
public class Config { public class MPConfig {
private static final Config instance = new Config(); private static final MPConfig instance = new MPConfig();
public static Config get() { public static MPConfig get() {
return instance; return instance;
} }

View File

@@ -24,8 +24,11 @@ import com.google.common.base.Charsets;
import com.google.common.io.ByteSource; import com.google.common.io.ByteSource;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ObjectUtils; import com.lyndir.lhunath.opal.system.util.ObjectUtils;
import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.gui.util.*;
import com.lyndir.masterpassword.gui.view.MasterPasswordFrame;
import com.lyndir.masterpassword.model.MPUser; import com.lyndir.masterpassword.model.MPUser;
import com.tulskiy.keymaster.common.Provider;
import java.awt.*;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.*; import java.net.*;
@@ -50,18 +53,21 @@ public final class MasterPassword {
private final Collection<Listener> listeners = new CopyOnWriteArraySet<>(); private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
@Nullable @Nullable
private MPUser<?> activeUser; private MasterPasswordFrame frame;
@Nullable
private MPUser<?> activeUser;
public static MasterPassword get() { public static MasterPassword get() {
return instance; return instance;
} }
public boolean addListener(final Listener listener) { public void addListener(final Listener listener) {
return listeners.add( listener ); if (listeners.add( listener ))
listener.onUserSelected( activeUser );
} }
public boolean removeListener(final Listener listener) { public void removeListener(final Listener listener) {
return listeners.remove( listener ); listeners.remove( listener );
} }
public void activateUser(final MPUser<?> user) { public void activateUser(final MPUser<?> user) {
@@ -78,6 +84,19 @@ public final class MasterPassword {
return MasterPassword.class.getPackage().getImplementationVersion(); return MasterPassword.class.getPackage().getImplementationVersion();
} }
public void open() {
Res.ui( () -> {
if (frame == null)
frame = new MasterPasswordFrame();
frame.setAlwaysOnTop( true );
frame.setVisible( true );
frame.setExtendedState( Frame.NORMAL );
Platform.get().requestForeground();
frame.setAlwaysOnTop( false );
} );
}
public void checkUpdate() { public void checkUpdate() {
try { try {
String implementationVersion = version(); String implementationVersion = version();
@@ -115,18 +134,22 @@ public final class MasterPassword {
// Try and set the system look & feel, if available. // Try and set the system look & feel, if available.
try { try {
UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() ); UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
Platform.get().installAppForegroundHandler( get()::open );
Platform.get().installAppReopenHandler( get()::open );
Provider.getCurrentProvider( true ).register( MPGuiConstants.ui_hotkey, hotKey -> get().open() );
} }
catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) { catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
} }
// Create a platform-specific GUI and open it. // Create a platform-specific GUI and open it.
new GUI().open(); get().open();
// Check online to see if this version has been superseded. // Check online to see if this version has been superseded.
if (Config.get().checkForUpdates()) if (MPConfig.get().checkForUpdates())
get().checkUpdate(); get().checkUpdate();
} }
@SuppressWarnings("InterfaceMayBeAnnotatedFunctional")
public interface Listener { public interface Listener {
void onUserSelected(@Nullable MPUser<?> user); void onUserSelected(@Nullable MPUser<?> user);

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
package com.lyndir.masterpassword.gui.model;
import com.lyndir.masterpassword.model.MPSite;
import com.lyndir.masterpassword.model.impl.MPBasicQuestion;
/**
* @author lhunath, 2018-07-27
*/
public class MPNewQuestion extends MPBasicQuestion {
public MPNewQuestion(final MPSite<?> site, final String keyword) {
super( site, keyword, site.getAlgorithm().mpw_default_answer_type() );
}
}

View File

@@ -1,7 +1,8 @@
package com.lyndir.masterpassword.gui.model; package com.lyndir.masterpassword.gui.model;
import com.lyndir.masterpassword.model.*; import com.lyndir.masterpassword.model.*;
import com.lyndir.masterpassword.model.impl.*; import com.lyndir.masterpassword.model.impl.MPBasicSite;
import javax.annotation.Nonnull;
/** /**
@@ -12,4 +13,20 @@ public class MPNewSite extends MPBasicSite<MPUser<?>, MPQuestion> {
public MPNewSite(final MPUser<?> user, final String siteName) { public MPNewSite(final MPUser<?> user, final String siteName) {
super( user, siteName ); super( user, siteName );
} }
@Nonnull
@Override
public MPQuestion addQuestion(final String keyword) {
throw new UnsupportedOperationException( "Cannot add a question to a site that hasn't been created yet." );
}
public <S extends MPSite<?>> S addTo(final MPUser<S> user) {
S site = user.addSite( getSiteName() );
site.setAlgorithm( getAlgorithm() );
site.setCounter( getCounter() );
site.setLoginType( getLoginType() );
site.setResultType( getResultType() );
return site;
}
} }

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
package com.lyndir.masterpassword.gui.util;
import java.util.function.Consumer;
import javax.annotation.Nullable;
/**
* @author lhunath, 2018-08-23
*/
public class ConsumingTrigger<T> implements Consumer<T> {
private final Runnable trigger;
@Nullable
private T value;
public ConsumingTrigger(final Runnable trigger) {
this.trigger = trigger;
}
@Override
public void accept(final T t) {
value = t;
trigger.run();
}
}

View File

@@ -0,0 +1,98 @@
package com.lyndir.masterpassword.gui.util;
import com.lyndir.lhunath.opal.system.logging.Logger;
import java.util.function.Consumer;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
/**
* @author lhunath, 2018-08-24
*/
public class DocumentModel implements Selectable<String, DocumentModel> {
private static final Logger logger = Logger.get( DocumentModel.class );
private final Document document;
@Nullable
private DocumentListener documentListener;
public DocumentModel(final Document document) {
this.document = document;
}
@Nonnull
public Document getDocument() {
return document;
}
@Nullable
public String getText() {
try {
return (document.getLength() > 0)? document.getText( 0, document.getLength() ): null;
}
catch (final BadLocationException e) {
logger.wrn( "While getting text for model", e );
return null;
}
}
public void setText(@Nullable final String text) {
try {
if (document.getLength() > 0)
document.remove( 0, document.getLength() );
if (text != null)
document.insertString( 0, text, null );
}
catch (final BadLocationException e) {
logger.err( "While setting text for model", e );
}
}
@Override
public DocumentModel selection(@Nullable final Consumer<String> selectionConsumer) {
if (documentListener != null)
document.removeDocumentListener( documentListener );
if (selectionConsumer != null)
document.addDocumentListener( documentListener = new DocumentListener() {
@Override
public void insertUpdate(final DocumentEvent e) {
trigger();
}
@Override
public void removeUpdate(final DocumentEvent e) {
trigger();
}
@Override
public void changedUpdate(final DocumentEvent e) {
trigger();
}
private void trigger() {
selectionConsumer.accept( getText() );
}
} );
return this;
}
@Override
public DocumentModel selection(@Nullable final String selectedItem, @Nullable final Consumer<String> selectionConsumer) {
setText( selectedItem );
selection( selectionConsumer );
if (selectionConsumer != null)
selectionConsumer.accept( selectedItem );
return this;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,12 +3,10 @@ package com.lyndir.masterpassword.gui.view;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.gui.util.Components;
import com.lyndir.masterpassword.gui.util.Res; import com.lyndir.masterpassword.gui.util.Res;
import com.lyndir.masterpassword.model.MPUser;
import com.lyndir.masterpassword.model.impl.MPFileUserManager; import com.lyndir.masterpassword.model.impl.MPFileUserManager;
import java.awt.*; import java.awt.*;
import java.awt.event.ComponentAdapter; import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent; import java.awt.event.ComponentEvent;
import javax.annotation.Nullable;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.BevelBorder; import javax.swing.border.BevelBorder;
@@ -21,26 +19,24 @@ public class MasterPasswordFrame extends JFrame {
private static final Logger logger = Logger.get( MasterPasswordFrame.class ); private static final Logger logger = Logger.get( MasterPasswordFrame.class );
@SuppressWarnings("FieldCanBeLocal") private final UserContentPanel userContent;
private final Components.GradientPanel root = Components.borderPanel( Res.colors().frameBg(), BoxLayout.PAGE_AXIS );
private final FilesPanel filesPanel = new FilesPanel();
private final JPanel userPanel = Components.panel( new BorderLayout( 0, 0 ) );
private final UserContentPanel userContent = new UserContentPanel();
@SuppressWarnings("MagicNumber") @SuppressWarnings("MagicNumber")
public MasterPasswordFrame() { public MasterPasswordFrame() {
super( "Master Password" ); super( "Master Password" );
setContentPane( root ); JPanel root, userPanel;
root.add( filesPanel ); setContentPane( root = Components.borderPanel( Res.colors().frameBg(), BoxLayout.PAGE_AXIS ) );
root.add( Components.strut() );
root.add( userPanel ); root.add( new FilesPanel() );
root.add( Components.strut() );
root.add( userPanel = Components.panel( new BorderLayout( 0, 0 ) ) );
userPanel.add( userContent.getUserToolbar(), BorderLayout.LINE_START );
userPanel.add( userContent.getSiteToolbar(), BorderLayout.LINE_END );
userPanel.add( Components.borderPanel( userPanel.add( Components.borderPanel(
BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ), BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ),
Res.colors().controlBg(), BoxLayout.PAGE_AXIS, userContent ), BorderLayout.CENTER ); Res.colors().controlBg(), BoxLayout.PAGE_AXIS, userContent = new UserContentPanel() ), BorderLayout.CENTER );
userPanel.add( userContent.getUserToolbar(), BorderLayout.LINE_START );
userPanel.add( userContent.getSiteToolbar(), BorderLayout.LINE_END );
addComponentListener( new ComponentHandler() ); addComponentListener( new ComponentHandler() );
setPreferredSize( new Dimension( 800, 560 ) ); setPreferredSize( new Dimension( 800, 560 ) );

View File

@@ -5,13 +5,13 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.base.*; import com.google.common.base.*;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.google.common.util.concurrent.ListenableFuture;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ObjectUtils; import com.lyndir.lhunath.opal.system.util.ObjectUtils;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.gui.MPGuiConstants; import com.lyndir.masterpassword.gui.MPGuiConstants;
import com.lyndir.masterpassword.gui.MasterPassword; import com.lyndir.masterpassword.gui.MasterPassword;
import com.lyndir.masterpassword.gui.model.MPIncognitoUser; import com.lyndir.masterpassword.gui.model.*;
import com.lyndir.masterpassword.gui.model.MPNewSite;
import com.lyndir.masterpassword.gui.util.*; import com.lyndir.masterpassword.gui.util.*;
import com.lyndir.masterpassword.gui.util.Platform; import com.lyndir.masterpassword.gui.util.Platform;
import com.lyndir.masterpassword.model.*; import com.lyndir.masterpassword.model.*;
@@ -28,12 +28,13 @@ import java.util.*;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.regex.Pattern;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.swing.*; import javax.swing.*;
import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener; import javax.swing.event.DocumentListener;
import javax.swing.text.PlainDocument;
/** /**
@@ -42,9 +43,12 @@ import javax.swing.event.DocumentListener;
@SuppressWarnings("SerializableStoresNonSerializable") @SuppressWarnings("SerializableStoresNonSerializable")
public class UserContentPanel extends JPanel implements MasterPassword.Listener, MPUser.Listener { public class UserContentPanel extends JPanel implements MasterPassword.Listener, MPUser.Listener {
private static final Random random = new Random(); private static final Random random = new Random();
private static final Logger logger = Logger.get( UserContentPanel.class ); private static final int SIZE_RESULT = 48;
private static final JButton iconButton = Components.button( Res.icons().user(), null, null ); 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(), private final JButton addButton = Components.button( Res.icons().add(), event -> addUser(),
"Add a new user to Master Password." ); "Add a new user to Master Password." );
@@ -290,9 +294,9 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
private final JButton resetButton = Components.button( Res.icons().reset(), event -> resetUser(), private final JButton resetButton = Components.button( Res.icons().reset(), event -> resetUser(),
"Change the master password for this user." ); "Change the master password for this user." );
private final JPasswordField masterPasswordField = Components.passwordField(); private final JPasswordField masterPasswordField;
private final JLabel errorLabel = Components.label(); private final JLabel errorLabel;
private final JLabel identiconLabel = Components.label( SwingConstants.CENTER ); private final JLabel identiconLabel;
private Future<?> identiconJob; private Future<?> identiconJob;
@@ -312,16 +316,16 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) ); add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
add( Components.strut() ); add( Components.strut() );
add( identiconLabel ); add( identiconLabel = Components.label( SwingConstants.CENTER ) );
identiconLabel.setFont( Res.fonts().emoticonsFont( Components.TEXT_SIZE_CONTROL ) ); identiconLabel.setFont( Res.fonts().emoticonsFont( Components.TEXT_SIZE_CONTROL ) );
add( Box.createGlue() ); add( Box.createGlue() );
add( Components.label( "Master Password:" ) ); add( Components.label( "Master Password:" ) );
add( Components.strut() ); add( Components.strut() );
add( masterPasswordField ); add( masterPasswordField = Components.passwordField() );
masterPasswordField.addActionListener( this ); masterPasswordField.addActionListener( this );
masterPasswordField.getDocument().addDocumentListener( this ); masterPasswordField.getDocument().addDocumentListener( this );
add( errorLabel ); add( errorLabel = Components.label() );
errorLabel.setForeground( Res.colors().errorFg() ); errorLabel.setForeground( Res.colors().errorFg() );
add( Box.createGlue() ); add( Box.createGlue() );
} }
@@ -458,9 +462,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} }
private final class AuthenticatedUserPanel extends JPanel implements KeyListener, MPUser.Listener { private final class AuthenticatedUserPanel extends JPanel implements KeyListener, MPUser.Listener, KeyEventDispatcher {
public static final int SIZE_RESULT = 48;
private final JButton userButton = Components.button( Res.icons().user(), event -> showUserPreferences(), private final JButton userButton = Components.button( Res.icons().user(), event -> showUserPreferences(),
"Show user preferences." ); "Show user preferences." );
@@ -468,22 +470,28 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
"Sign out and lock user." ); "Sign out and lock user." );
private final JButton settingsButton = Components.button( Res.icons().settings(), event -> showSiteSettings(), private final JButton settingsButton = Components.button( Res.icons().settings(), event -> showSiteSettings(),
"Show site settings." ); "Show site settings." );
private final JButton questionsButton = Components.button( Res.icons().question(), null, private final JButton questionsButton = Components.button( Res.icons().question(), event -> showSiteQuestions(),
"Show site recovery questions." ); "Show site recovery questions." );
private final JButton editButton = Components.button( Res.icons().edit(), event -> showSiteValues(),
"Set/save personal password/login." );
private final JButton keyButton = Components.button( Res.icons().key(), event -> showSiteKeys(),
"Cryptographic site keys." );
private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteSite(), private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteSite(),
"Delete the site from the user." ); "Delete the site from the user." );
@Nonnull @Nonnull
private final MPUser<?> user; private final MPUser<?> user;
private final JLabel passwordLabel = Components.label( SwingConstants.CENTER ); private final JLabel resultLabel;
private final JLabel passwordField = Components.heading( SwingConstants.CENTER ); private final JLabel resultField;
private final JLabel queryLabel = Components.label(); private final JLabel answerLabel;
private final JTextField queryField = Components.textField( null, this::updateSites ); private final JLabel answerField;
private final CollectionListModel<MPSite<?>> sitesModel = private final JLabel queryLabel;
new CollectionListModel<MPSite<?>>().selection( this::showSiteResult ); private final JTextField queryField;
private final JList<MPSite<?>> sitesList = private final CollectionListModel<MPQuery.Result<? extends MPSite<?>>> sitesModel;
Components.list( sitesModel, this::getSiteDescription ); private final CollectionListModel<MPQuery.Result<? extends MPQuestion>> questionsModel;
private final JList<MPQuery.Result<? extends MPSite<?>>> sitesList;
private boolean showLogin;
private Future<?> updateSitesJob; private Future<?> updateSitesJob;
private AuthenticatedUserPanel(@Nonnull final MPUser<?> user) { private AuthenticatedUserPanel(@Nonnull final MPUser<?> user) {
@@ -499,53 +507,85 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
siteToolbar.add( settingsButton ); siteToolbar.add( settingsButton );
siteToolbar.add( questionsButton ); siteToolbar.add( questionsButton );
siteToolbar.add( editButton );
siteToolbar.add( keyButton );
siteToolbar.add( deleteButton ); siteToolbar.add( deleteButton );
settingsButton.setEnabled( false ); settingsButton.setEnabled( false );
questionsButton.setEnabled( false );
editButton.setEnabled( false );
keyButton.setEnabled( false );
deleteButton.setEnabled( false ); deleteButton.setEnabled( false );
answerLabel = Components.label( "Answer:" );
answerField = Components.heading( SwingConstants.CENTER );
answerField.setForeground( Res.colors().highlightFg() );
answerField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
questionsModel = new CollectionListModel<MPQuery.Result<? extends MPQuestion>>().selection( this::showQuestionItem );
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) ); add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
add( passwordLabel ); add( resultLabel = Components.label( SwingConstants.CENTER ) );
add( passwordField ); add( resultField = Components.heading( SwingConstants.CENTER ) );
passwordField.setForeground( Res.colors().highlightFg() ); resultField.setForeground( Res.colors().highlightFg() );
passwordField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) ); resultField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
add( Box.createGlue() ); add( Box.createGlue() );
add( Components.strut() ); add( Components.strut() );
add( queryLabel ); add( queryLabel = Components.label() );
queryLabel.setText( strf( "%s's password for:", user.getFullName() ) ); queryLabel.setText( strf( "%s's password for:", user.getFullName() ) );
add( queryField ); add( queryField = Components.textField( null, this::updateSites ) );
queryField.putClientProperty( "JTextField.variant", "search" ); queryField.putClientProperty( "JTextField.variant", "search" );
queryField.addActionListener( event -> useSite() ); queryField.addActionListener( this::useSite );
queryField.getInputMap().put( copyLoginKeyStroke, JTextField.notifyAction );
queryField.addKeyListener( this ); queryField.addKeyListener( this );
queryField.requestFocusInWindow(); queryField.requestFocusInWindow();
add( Components.strut() ); add( Components.strut() );
add( Components.scrollPane( sitesList ) );
sitesModel.registerList( sitesList ); add( Components.scrollPane( sitesList = Components.list(
add( Box.createGlue() ); sitesModel = new CollectionListModel<MPQuery.Result<? extends MPSite<?>>>().selection( this::showSiteItem ),
this::getSiteDescription ) ) );
sitesList.registerKeyboardAction( this::useSite, KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 ),
JComponent.WHEN_FOCUSED );
sitesList.registerKeyboardAction( this::useSite, KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK ),
JComponent.WHEN_FOCUSED );
add( Components.strut() );
add( Components.label( strf(
"Press %s to copy password, %s+%s to copy login name.",
KeyEvent.getKeyText( KeyEvent.VK_ENTER ),
InputEvent.getModifiersExText( copyLoginKeyStroke.getModifiers() ),
KeyEvent.getKeyText( copyLoginKeyStroke.getKeyCode() ) ) ) );
addHierarchyListener( e -> { addHierarchyListener( e -> {
if (null != SwingUtilities.windowForComponent( this )) if (HierarchyEvent.DISPLAYABILITY_CHANGED == (e.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED)) {
user.addListener( this ); if (null != SwingUtilities.windowForComponent( this )) {
else KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher( this );
user.removeListener( this ); user.addListener( this );
} else {
KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher( this );
user.removeListener( this );
}
}
} ); } );
} }
public void showUserPreferences() { public void showUserPreferences() {
ImmutableList.Builder<Component> components = ImmutableList.builder(); ImmutableList.Builder<Component> components = ImmutableList.builder();
components.add( Components.label( "Default Algorithm:" ),
Components.comboBox( MPAlgorithm.Version.values(), MPAlgorithm.Version::name,
user.getAlgorithm().version(),
version -> user.setAlgorithm( version.getAlgorithm() ) ) );
MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null; MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null;
if (fileUser != null) if (fileUser != null) {
components.add( Components.label( "Default Password Type:" ), components.add( Components.label( "Default Password Type:" ),
Components.comboBox( MPResultType.values(), MPResultType::getLongName, Components.comboBox( MPResultType.values(), MPResultType::getLongName,
fileUser.getDefaultType(), fileUser::setDefaultType ), fileUser.getDefaultType(), fileUser::setDefaultType ),
Components.strut() ); Components.strut() );
components.add( Components.label( "Default Algorithm:" ), components.add( Components.checkBox( "Hide Passwords", fileUser.isHidePasswords(), fileUser::setHidePasswords ) );
Components.comboBox( MPAlgorithm.Version.values(), MPAlgorithm.Version::name, }
user.getAlgorithm().version(),
version -> user.setAlgorithm( version.getAlgorithm() ) ) );
Components.showDialog( this, user.getFullName(), new JOptionPane( Components.panel( Components.showDialog( this, user.getFullName(), new JOptionPane( Components.panel(
BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) ); BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) );
@@ -556,7 +596,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} }
public void showSiteSettings() { public void showSiteSettings() {
MPSite<?> site = sitesModel.getSelectedItem(); MPSite<?> site = getSite();
if (site == null) if (site == null)
return; return;
@@ -572,12 +612,14 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
Components.strut() ); Components.strut() );
components.add( Components.label( "Password Type:" ), components.add( Components.label( "Password Type:" ),
Components.comboBox( MPResultType.values(), MPResultType::getLongName, Components.comboBox( MPResultType.values(), type -> getTypeDescription(
type, user.getDefaultType(), user.getAlgorithm().mpw_default_result_type() ),
site.getResultType(), site::setResultType ), site.getResultType(), site::setResultType ),
Components.strut() ); Components.strut() );
components.add( Components.label( "Login Type:" ), components.add( Components.label( "Login Type:" ),
Components.comboBox( MPResultType.values(), MPResultType::getLongName, Components.comboBox( MPResultType.values(), type -> getTypeDescription(
type, user.getAlgorithm().mpw_default_login_type() ),
site.getLoginType(), site::setLoginType ), site.getLoginType(), site::setLoginType ),
Components.strut() ); Components.strut() );
@@ -587,12 +629,181 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
Components.textField( fileSite.getUrl(), fileSite::setUrl ), Components.textField( fileSite.getUrl(), fileSite::setUrl ),
Components.strut() ); Components.strut() );
Components.showDialog( this, site.getSiteName(), new JOptionPane( Components.panel( Components.showDialog( this, strf( "Settings for %s", site.getSiteName() ), new JOptionPane( Components.panel(
BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) ); BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) );
} }
private String getTypeDescription(final MPResultType type, final MPResultType... defaults) {
boolean isDefault = false;
for (final MPResultType d : defaults)
if (isDefault = type == d)
break;
return strf( "<html>%s%s%s, %s", isDefault? "<b>": "", type.getLongName(), isDefault? "</b>": "", type.getDescription() );
}
public void showSiteQuestions() {
MPSite<?> site = getSite();
if (site == null)
return;
JList<MPQuery.Result<? extends MPQuestion>> questionsList =
Components.list( questionsModel, this::getQuestionDescription );
JTextField queryField = Components.textField( null, queryText -> Res.job( () -> {
MPQuery query = new MPQuery( queryText );
Collection<MPQuery.Result<? extends MPQuestion>> questionItems = new LinkedList<>( site.findQuestions( query ) );
if (questionItems.stream().noneMatch( MPQuery.Result::isExact ))
questionItems.add( MPQuery.Result.allOf( new MPNewQuestion( site, query.getQuery() ), query.getQuery() ) );
Res.ui( () -> questionsModel.set( questionItems ) );
} ) );
queryField.putClientProperty( "JTextField.variant", "search" );
queryField.addActionListener( this::useQuestion );
queryField.addKeyListener( new KeyAdapter() {
@Override
public void keyPressed(final KeyEvent event) {
if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN))
questionsList.dispatchEvent( event );
}
@Override
public void keyReleased(final KeyEvent event) {
if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN))
questionsList.dispatchEvent( event );
}
} );
Components.showDialog( this, strf( "Recovery answers for %s", site.getSiteName() ), new JOptionPane( Components.panel(
BoxLayout.PAGE_AXIS,
Components.label( "Security Question Keyword:" ), queryField,
Components.strut(),
answerLabel, answerField,
Components.strut(),
Components.scrollPane( questionsList ) ) ) {
@Override
public void selectInitialValue() {
queryField.requestFocusInWindow();
}
} );
}
public void showSiteValues() {
MPSite<?> site = getSite();
if (site == null)
return;
try {
JTextField passwordField = Components.textField( site.getResult(), null );
JTextField loginField = Components.textField( site.getLogin(), null );
passwordField.setEditable( site.getResultType().getTypeClass() == MPResultTypeClass.Stateful );
loginField.setEditable( site.getLoginType().getTypeClass() == MPResultTypeClass.Stateful );
if (JOptionPane.OK_OPTION == Components.showDialog( this, site.getSiteName(), new JOptionPane(
Components.panel(
BoxLayout.PAGE_AXIS,
Components.label( strf( "<html>Site Login (currently set to: <b>%s</b>):",
getTypeDescription( site.getLoginType() ) ) ),
loginField,
Components.strut(),
Components.label( strf( "<html>Site Password (currently set to: <b>%s</b>):",
getTypeDescription( site.getResultType() ) ) ),
passwordField,
Components.strut(),
Components.label( "<html>To save a personal value in these fields,\n" +
"change the type to <b>Saved</b> in the site's settings." ) ),
JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION ) {
@Override
public void selectInitialValue() {
passwordField.requestFocusInWindow();
}
} )) {
if (site instanceof MPFileSite) {
MPFileSite fileSite = (MPFileSite) site;
if (site.getResultType().getTypeClass() == MPResultTypeClass.Stateful)
fileSite.setSitePassword( site.getResultType(), passwordField.getText() );
if (site.getLoginType().getTypeClass() == MPResultTypeClass.Stateful)
fileSite.setLoginName( site.getLoginType(), loginField.getText() );
}
}
}
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
logger.err( e, "While computing site edit results." );
}
}
public void showSiteKeys() {
MPSite<?> site = getSite();
if (site == null)
return;
JTextArea resultField = Components.textArea();
resultField.setEnabled( false );
CollectionListModel<MPKeyPurpose> purposeModel = new CollectionListModel<>( MPKeyPurpose.values() );
DocumentModel contextModel = new DocumentModel( new PlainDocument() );
UnsignedIntegerModel counterModel = new UnsignedIntegerModel( UnsignedInteger.ONE );
CollectionListModel<MPResultType> typeModel = new CollectionListModel<>( MPResultType.values() );
DocumentModel stateModel = new DocumentModel( new PlainDocument() );
Runnable trigger = () -> Res.job( () -> {
try {
MPKeyPurpose purpose = purposeModel.getSelectedItem();
MPResultType type = typeModel.getSelectedItem();
String result = ((purpose == null) || (type == null))? null:
site.getResult( purpose, contextModel.getText(), counterModel.getNumber(), type, stateModel.getText() );
Res.ui( () -> resultField.setText( result ) );
}
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
logger.err( e, "While computing site edit results." );
}
} );
purposeModel.selection( MPKeyPurpose.Authentication, p -> trigger.run() );
contextModel.selection( c -> trigger.run() );
counterModel.selection( c -> trigger.run() );
typeModel.selection( MPResultType.DeriveKey, t -> {
switch (t) {
case DeriveKey:
stateModel.setText( Integer.toString( site.getAlgorithm().mpw_keySize_min() ) );
break;
default:
stateModel.setText( null );
}
trigger.run();
} );
stateModel.selection( c -> trigger.run() );
if (JOptionPane.OK_OPTION == Components.showDialog( this, site.getSiteName(), new JOptionPane( Components.panel(
BoxLayout.PAGE_AXIS,
Components.heading( "Key Calculator" ),
Components.label( "Purpose:" ),
Components.comboBox( purposeModel, MPKeyPurpose::getShortName ),
Components.strut(),
Components.label( "Context:" ),
Components.textField( contextModel.getDocument() ),
Components.label( "Counter:" ),
Components.spinner( counterModel ),
Components.label( "Type:" ),
Components.comboBox( typeModel, this::getTypeDescription ),
Components.label( "State:" ),
Components.scrollPane( Components.textField( stateModel.getDocument() ) ),
Components.strut(),
resultField ) ) {
{
setOptions( new Object[]{ "Copy", "Cancel" } );
setInitialValue( getOptions()[0] );
}
} ))
copyResult( resultField.getText() );
}
public void deleteSite() { public void deleteSite() {
MPSite<?> site = sitesModel.getSelectedItem(); MPSite<?> site = getSite();
if (site == null) if (site == null)
return; return;
@@ -602,96 +813,193 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
user.deleteSite( site ); user.deleteSite( site );
} }
private String getSiteDescription(@Nonnull final MPSite<?> site) { private String getSiteDescription(@Nullable final MPQuery.Result<? extends MPSite<?>> item) {
MPSite<?> site = (item != null)? item.getOption(): null;
if (site == null)
return " ";
if (site instanceof MPNewSite) if (site instanceof MPNewSite)
return strf( "<html><strong>%s</strong> &lt;Add new site&gt;</html>", queryField.getText() ); return strf( "<html><strong>%s</strong> &lt;Add new site&gt;</html>", item.getKeyAsHTML() );
ImmutableList.Builder<Object> parameters = ImmutableList.builder(); ImmutableList.Builder<Object> parameters = ImmutableList.builder();
try { MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null;
MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null; if (fileSite != null)
if (fileSite != null) parameters.add( Res.format( fileSite.getLastUsed() ) );
parameters.add( Res.format( fileSite.getLastUsed() ) ); parameters.add( site.getAlgorithm().version() );
parameters.add( site.getAlgorithm().version() ); parameters.add( strf( "#%d", site.getCounter().longValue() ) );
parameters.add( strf( "#%d", site.getCounter().longValue() ) ); if ((fileSite != null) && (fileSite.getUrl() != null))
parameters.add( strf( "<em>%s</em>", site.getLogin() ) ); parameters.add( fileSite.getUrl() );
if ((fileSite != null) && (fileSite.getUrl() != null))
parameters.add( fileSite.getUrl() );
}
catch (final MPAlgorithmException | MPKeyUnavailableException e) {
logger.err( e, "While generating site description." );
parameters.add( e.getLocalizedMessage() );
}
return strf( "<html><strong>%s</strong> (%s)</html>", site.getSiteName(), return strf( "<html><strong>%s</strong> (%s)</html>", item.getKeyAsHTML(),
Joiner.on( " - " ).skipNulls().join( parameters.build() ) ); Joiner.on( " - " ).skipNulls().join( parameters.build() ) );
} }
private void useSite() { private String getQuestionDescription(@Nullable final MPQuery.Result<? extends MPQuestion> item) {
MPSite<?> site = sitesModel.getSelectedItem(); MPQuestion question = (item != null)? item.getOption(): 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 (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() ), this, strf( "<html>Remember the site <strong>%s</strong>?</html>", site.getSiteName() ),
"New Site", JOptionPane.YES_NO_OPTION )) { "New Site", JOptionPane.YES_NO_OPTION ))
sitesModel.setSelectedItem( user.addSite( site.getSiteName() ) ); return;
useSite();
} site = ((MPNewSite) site).addTo( user );
return;
} }
showSiteResult( site, result -> { boolean loginResult = (copyLoginKeyStroke.getModifiers() & event.getModifiers()) != 0;
MPSite<?> fsite = site;
Res.ui( getSiteResult( site, loginResult ), result -> {
if (result == null) if (result == null)
return; return;
if (site instanceof MPFileSite) if (fsite instanceof MPFileSite)
((MPFileSite) site).use(); ((MPFileSite) fsite).use();
Transferable clipboardContents = new StringSelection( result ); copyResult( result );
Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null );
Res.ui( () -> {
Window window = SwingUtilities.windowForComponent( UserContentPanel.this );
if (window instanceof Frame)
((Frame) window).setExtendedState( Frame.ICONIFIED );
} );
} ); } );
} }
private void showSiteResult(@Nullable final MPSite<?> site) { private void setShowLogin(final boolean showLogin) {
showSiteResult( site, null ); if (showLogin == this.showLogin)
return;
this.showLogin = showLogin;
showSiteItem( sitesModel.getSelectedItem() );
} }
private void showSiteResult(@Nullable final MPSite<?> site, @Nullable final Consumer<String> resultCallback) { private void showSiteItem(@Nullable final MPQuery.Result<? extends MPSite<?>> item) {
if (site == null) { MPSite<?> site = (item != null)? item.getOption(): null;
if (resultCallback != null) Res.ui( getSiteResult( site, showLogin ), result -> {
resultCallback.accept( null ); if (!showLogin && (site != null))
Res.ui( () -> { resultLabel.setText( (result != null)? strf( "Your password for %s:", site.getSiteName() ): " " );
passwordLabel.setText( " " ); else if (showLogin && (site != null))
passwordField.setText( " " ); resultLabel.setText( (result != null)? strf( "Your login for %s:", site.getSiteName() ): " " );
settingsButton.setEnabled( false );
deleteButton.setEnabled( false );
} );
return;
}
Res.job( () -> { if ((result == null) || result.isEmpty())
resultField.setText( " " );
else if (!showLogin && (user instanceof MPFileUser) && ((MPFileUser) user).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 { try {
String result = site.getResult(); if (site != null)
if (resultCallback != null) return loginResult? site.getLogin(): site.getResult();
resultCallback.accept( result );
Res.ui( () -> {
passwordLabel.setText( strf( "Your password for %s:", site.getSiteName() ) );
passwordField.setText( result );
settingsButton.setEnabled( true );
deleteButton.setEnabled( true );
} );
} }
catch (final MPKeyUnavailableException | MPAlgorithmException e) { catch (final MPKeyUnavailableException | MPAlgorithmException e) {
logger.err( e, "While resolving password for: %s", site ); logger.err( e, "While resolving password for: %s", site );
} }
return null;
} ); } );
} }
private void useQuestion(final ActionEvent event) {
MPQuestion question = getQuestion();
if (question instanceof MPNewQuestion) {
if (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(
this,
strf( "<html>Remember the security question with keyword <strong>%s</strong>?</html>",
Strings.isNullOrEmpty( question.getKeyword() )? "<empty>": question.getKeyword() ),
"New Question", JOptionPane.YES_NO_OPTION ))
return;
question = question.getSite().addQuestion( question.getKeyword() );
}
MPQuestion fquestion = question;
Res.ui( getQuestionResult( question ), result -> {
if (result == null)
return;
if (fquestion instanceof MPFileQuestion)
((MPFileQuestion) fquestion).use();
copyResult( result );
} );
}
private void showQuestionItem(@Nullable final MPQuery.Result<? extends MPQuestion> item) {
MPQuestion question = (item != null)? item.getOption(): 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.getOption();
}
@Nullable
private MPQuestion getQuestion() {
MPQuery.Result<? extends MPQuestion> selectedQuestion = questionsModel.getSelectedItem();
if (selectedQuestion == null)
return null;
return selectedQuestion.getOption();
}
@Override @Override
public void keyTyped(final KeyEvent event) { public void keyTyped(final KeyEvent event) {
} }
@@ -708,27 +1016,33 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
sitesList.dispatchEvent( event ); sitesList.dispatchEvent( event );
} }
private synchronized void updateSites(@Nullable final String query) { private synchronized void updateSites(@Nullable final String queryText) {
if (updateSitesJob != null) if (updateSitesJob != null)
updateSitesJob.cancel( true ); updateSitesJob.cancel( true );
updateSitesJob = Res.job( () -> { updateSitesJob = Res.job( () -> {
Collection<MPSite<?>> sites = new LinkedList<>(); MPQuery query = new MPQuery( queryText );
if (!Strings.isNullOrEmpty( query )) { Collection<MPQuery.Result<? extends MPSite<?>>> siteItems =
sites.addAll( new LinkedList<>( user.findSites( query ) ) ); new LinkedList<>( user.findSites( query ) );
if (sites.stream().noneMatch( site -> site.getSiteName().equalsIgnoreCase( query ) )) if (!Strings.isNullOrEmpty( queryText ))
sites.add( new MPNewSite( user, query ) ); if (siteItems.stream().noneMatch( MPQuery.Result::isExact )) {
} MPQuery.Result<? extends MPSite<?>> selectedItem = sitesModel.getSelectedItem();
if ((selectedItem != null) && user.equals( selectedItem.getOption().getUser() ) &&
queryText.equals( selectedItem.getOption().getSiteName() ))
siteItems.add( selectedItem );
else
siteItems.add( MPQuery.Result.allOf( new MPNewSite( user, query.getQuery() ), query.getQuery() ) );
}
Res.ui( () -> sitesModel.set( sites ) ); Res.ui( () -> sitesModel.set( siteItems ) );
} ); } );
} }
@Override @Override
public void onUserUpdated(final MPUser<?> user) { public void onUserUpdated(final MPUser<?> user) {
updateSites( queryField.getText() ); updateSites( queryField.getText() );
showSiteResult( sitesModel.getSelectedItem() ); showSiteItem( sitesModel.getSelectedItem() );
} }
@Override @Override
@@ -738,5 +1052,13 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
@Override @Override
public void onUserInvalidated(final MPUser<?> user) { public void onUserInvalidated(final MPUser<?> user) {
} }
@Override
public boolean dispatchKeyEvent(final KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT)
setShowLogin( e.isShiftDown() );
return false;
}
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,157 @@
package com.lyndir.masterpassword.model;
import static com.lyndir.lhunath.opal.system.util.StringUtils.strf;
import java.util.*;
import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.jetbrains.annotations.NotNull;
/**
* @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 <T extends Comparable<? super T>> Optional<Result<T>> find(final T option, final Function<T, CharSequence> keyForOption) {
CharSequence key = keyForOption.apply( option );
Result<T> result = Result.noneOf( option, 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 query is consumed, the result is a hit.
return (q >= query.length())? Optional.of( result ): Optional.empty();
}
public static class Result<T extends Comparable<? super T>> implements Comparable<Result<T>> {
private final T option;
private final CharSequence key;
private final boolean[] keyMatches;
Result(final T option, final CharSequence key) {
this.option = option;
this.key = key;
keyMatches = new boolean[key.length()];
}
public static <T extends Comparable<? super T>> Result<T> noneOf(final T option, final CharSequence key) {
return new Result<>( option, key );
}
public static <T extends Comparable<? super T>> Result<T> allOf(final T option, final CharSequence key) {
Result<T> result = noneOf( option, key );
Arrays.fill( result.keyMatches, true );
return result;
}
@Nonnull
public T getOption() {
return option;
}
@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 int compareTo(@NotNull final Result<T> o) {
return getOption().compareTo( o.getOption() );
}
@Override
public boolean equals(final Object o) {
if (!(o instanceof Result))
return false;
Result<?> r = (Result<?>) o;
return Objects.equals( option, r.option ) && Objects.equals( key, r.key ) && Arrays.equals( keyMatches, r.keyMatches );
}
@Override
public int hashCode() {
return getOption().hashCode();
}
@Override
public String toString() {
return strf( "{Result: %s}", key );
}
}
}

View File

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

View File

@@ -18,6 +18,7 @@
package com.lyndir.masterpassword.model; package com.lyndir.masterpassword.model;
import com.google.common.collect.ImmutableCollection;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import java.util.Collection; import java.util.Collection;
@@ -57,35 +58,48 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
void setLoginType(@Nullable MPResultType loginType); void setLoginType(@Nullable MPResultType loginType);
@Nullable
default String getResult() default String getResult()
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
return getResult( MPKeyPurpose.Authentication ); return getResult( MPKeyPurpose.Authentication );
} }
@Nonnull @Nullable
default String getResult(final MPKeyPurpose keyPurpose) default String getResult(final MPKeyPurpose keyPurpose)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
return getResult( keyPurpose, null ); return getResult( keyPurpose, null );
} }
@Nonnull @Nullable
default String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext) default String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
return getResult( keyPurpose, keyContext, null ); return getResult( keyPurpose, keyContext, null );
} }
@Nonnull @Nullable
String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state) String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state)
throws MPKeyUnavailableException, MPAlgorithmException; throws MPKeyUnavailableException, MPAlgorithmException;
/**
* @see MPMasterKey#siteResult(String, MPAlgorithm, UnsignedInteger, MPKeyPurpose, String, MPResultType, String)
*/
@Nullable
String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext,
@Nullable UnsignedInteger counter, MPResultType type, @Nullable String state)
throws MPKeyUnavailableException, MPAlgorithmException;
@Nonnull @Nonnull
String getState(MPKeyPurpose keyPurpose, @Nullable String keyContext,
@Nullable UnsignedInteger counter, MPResultType type, String state)
throws MPKeyUnavailableException, MPAlgorithmException;
@Nullable
default String getLogin() default String getLogin()
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
return getLogin( null ); return getLogin( null );
} }
@Nonnull @Nullable
String getLogin(@Nullable String state) String getLogin(@Nullable String state)
throws MPKeyUnavailableException, MPAlgorithmException; throws MPKeyUnavailableException, MPAlgorithmException;
@@ -94,10 +108,17 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
@Nonnull @Nonnull
MPUser<?> getUser(); MPUser<?> getUser();
boolean addQuestion(Q question); @Nonnull
Q addQuestion(String keyword);
@Nonnull
Q addQuestion(Q question);
boolean deleteQuestion(Q question); boolean deleteQuestion(Q question);
@Nonnull @Nonnull
Collection<Q> getQuestions(); Collection<Q> getQuestions();
@Nonnull
ImmutableCollection<MPQuery.Result<Q>> findQuestions(MPQuery query);
} }

View File

@@ -100,6 +100,7 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
// - Relations // - Relations
@Nonnull
S addSite(String siteName); S addSite(String siteName);
@Nonnull @Nonnull
@@ -111,11 +112,11 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
Collection<S> getSites(); Collection<S> getSites();
@Nonnull @Nonnull
ImmutableCollection<S> findSites(@Nullable String query); ImmutableCollection<MPQuery.Result<S>> findSites(MPQuery query);
boolean addListener(Listener listener); void addListener(Listener listener);
boolean removeListener(Listener listener); void removeListener(Listener listener);
interface Listener { interface Listener {

View File

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

View File

@@ -21,6 +21,8 @@ package com.lyndir.masterpassword.model.impl;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*; import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*; import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.*; import com.lyndir.masterpassword.model.*;
@@ -35,9 +37,9 @@ import javax.annotation.Nullable;
public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> extends Changeable public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> extends Changeable
implements MPSite<Q> { implements MPSite<Q> {
private final Collection<Q> questions = new LinkedHashSet<>();
private final U user; private final U user;
private final String siteName; private final String siteName;
private final Collection<Q> questions = new LinkedHashSet<>();
private MPAlgorithm algorithm; private MPAlgorithm algorithm;
private UnsignedInteger counter; private UnsignedInteger counter;
@@ -60,12 +62,16 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
this.loginType = (loginType != null)? loginType: this.algorithm.mpw_default_login_type(); this.loginType = (loginType != null)? loginType: this.algorithm.mpw_default_login_type();
} }
// - Meta
@Nonnull @Nonnull
@Override @Override
public String getSiteName() { public String getSiteName() {
return siteName; return siteName;
} }
// - Algorithm
@Nonnull @Nonnull
@Override @Override
public MPAlgorithm getAlgorithm() { public MPAlgorithm getAlgorithm() {
@@ -104,7 +110,7 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
@Override @Override
public void setResultType(final MPResultType resultType) { public void setResultType(final MPResultType resultType) {
if (Objects.equals( this.resultType, resultType )) if (this.resultType == resultType)
return; return;
this.resultType = resultType; this.resultType = resultType;
@@ -119,14 +125,14 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
@Override @Override
public void setLoginType(@Nullable final MPResultType loginType) { public void setLoginType(@Nullable final MPResultType loginType) {
if (Objects.equals( this.loginType, loginType )) if (this.loginType == loginType)
return; return;
this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() ); this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() );
setChanged(); setChanged();
} }
@Nonnull @Nullable
@Override @Override
public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nullable final String state) public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nullable final String state)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@@ -134,8 +140,10 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
return getResult( keyPurpose, keyContext, getCounter(), getResultType(), state ); return getResult( keyPurpose, keyContext, getCounter(), getResultType(), state );
} }
protected String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nullable
@Nullable final UnsignedInteger counter, final MPResultType type, @Nullable final String state) @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 { throws MPKeyUnavailableException, MPAlgorithmException {
return getUser().getMasterKey().siteResult( return getUser().getMasterKey().siteResult(
@@ -143,8 +151,10 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
keyPurpose, keyContext, type, state ); keyPurpose, keyContext, type, state );
} }
protected String getState(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nonnull
@Nullable final UnsignedInteger counter, final MPResultType type, final String state) @Override
public String getState(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
@Nullable final UnsignedInteger counter, final MPResultType type, final String state)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
return getUser().getMasterKey().siteState( return getUser().getMasterKey().siteState(
@@ -152,7 +162,7 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
keyPurpose, keyContext, type, state ); keyPurpose, keyContext, type, state );
} }
@Nonnull @Nullable
@Override @Override
public String getLogin(@Nullable final String state) public String getLogin(@Nullable final String state)
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
@@ -160,13 +170,21 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
return getResult( MPKeyPurpose.Identification, null, null, getLoginType(), state ); return getResult( MPKeyPurpose.Identification, null, null, getLoginType(), state );
} }
// - Relations
@Nonnull
@Override @Override
public boolean addQuestion(final Q question) { public U getUser() {
if (!questions.add( question )) return user;
return false; }
@Nonnull
@Override
public Q addQuestion(final Q question) {
questions.add( question );
setChanged(); setChanged();
return true; return question;
} }
@Override @Override
@@ -186,8 +204,12 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
@Nonnull @Nonnull
@Override @Override
public U getUser() { public ImmutableCollection<MPQuery.Result<Q>> findQuestions(final MPQuery query) {
return user; ImmutableSortedSet.Builder<MPQuery.Result<Q>> results = ImmutableSortedSet.naturalOrder();
for (final Q question : questions)
query.find( question, MPQuestion::getKeyword ).ifPresent( results::add );
return results.build();
} }
@Override @Override

View File

@@ -200,24 +200,22 @@ public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeabl
@Nonnull @Nonnull
@Override @Override
public ImmutableCollection<S> findSites(@Nullable final String query) { public ImmutableCollection<MPQuery.Result<S>> findSites(final MPQuery query) {
ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder(); ImmutableSortedSet.Builder<MPQuery.Result<S>> results = ImmutableSortedSet.naturalOrder();
if (query != null) for (final S site : sites.values())
for (final S site : getSites()) query.find( site, MPSite::getSiteName ).ifPresent( results::add );
if (site.getSiteName().startsWith( query ))
results.add( site );
return results.build(); return results.build();
} }
@Override @Override
public boolean addListener(final Listener listener) { public void addListener(final Listener listener) {
return listeners.add( listener ); listeners.add( listener );
} }
@Override @Override
public boolean removeListener(final Listener listener) { public void removeListener(final Listener listener) {
return listeners.remove( listener ); listeners.remove( listener );
} }
@Override @Override

View File

@@ -30,16 +30,13 @@ import javax.annotation.Nullable;
*/ */
public class MPFileQuestion extends MPBasicQuestion { public class MPFileQuestion extends MPBasicQuestion {
private final MPFileSite site;
@Nullable @Nullable
private String answerState; private String answerState;
public MPFileQuestion(final MPFileSite site, final String keyword, public MPFileQuestion(final MPFileSite site, final String keyword,
@Nullable final MPResultType type, @Nullable final String answerState) { @Nullable final MPResultType type, @Nullable final String answerState) {
super( keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) ); super( site, keyword, ifNotNullElse( type, site.getAlgorithm().mpw_default_answer_type() ) );
this.site = site;
this.answerState = answerState; this.answerState = answerState;
} }
@@ -48,6 +45,8 @@ public class MPFileQuestion extends MPBasicQuestion {
return answerState; return answerState;
} }
@Nonnull
@Override
public String getAnswer() public String getAnswer()
throws MPKeyUnavailableException, MPAlgorithmException { throws MPKeyUnavailableException, MPAlgorithmException {
return getAnswer( answerState ); return getAnswer( answerState );
@@ -66,9 +65,8 @@ public class MPFileQuestion extends MPBasicQuestion {
setChanged(); setChanged();
} }
@Nonnull public void use() {
@Override if (getSite() instanceof MPFileSite)
public MPFileSite getSite() { ((MPFileSite) getSite()).use();
return site;
} }
} }

View File

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

View File

@@ -47,6 +47,7 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
private MPResultType defaultType; private MPResultType defaultType;
private ReadableInstant lastUsed; private ReadableInstant lastUsed;
private boolean hidePasswords;
private boolean complete; private boolean complete;
@Nullable @Nullable
@@ -54,7 +55,7 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
throws IOException, MPMarshalException { throws IOException, MPMarshalException {
for (final MPMarshalFormat format : MPMarshalFormat.values()) for (final MPMarshalFormat format : MPMarshalFormat.values())
if (file.getName().endsWith( format.fileSuffix() )) if (file.getName().endsWith( format.fileSuffix() ))
return format.unmarshaller().readUser( file ); return format.unmarshaller().readUser( file );
return null; return null;
} }
@@ -64,18 +65,19 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
} }
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final File path) { public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final File path) {
this( fullName, keyID, algorithm, 0, null, new Instant(), this( fullName, keyID, algorithm, 0, null, new Instant(), false,
MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, path ); MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, path );
} }
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final int avatar,
final int avatar, @Nullable final MPResultType defaultType, final ReadableInstant lastUsed, @Nullable final MPResultType defaultType, final ReadableInstant lastUsed, final boolean hidePasswords,
final MPMarshaller.ContentMode contentMode, final MPMarshalFormat format, final File path) { final MPMarshaller.ContentMode contentMode, final MPMarshalFormat format, final File path) {
super( avatar, fullName, algorithm ); super( avatar, fullName, algorithm );
this.keyID = (keyID != null)? keyID.clone(): null; this.keyID = (keyID != null)? keyID.clone(): null;
this.defaultType = (defaultType != null)? defaultType: algorithm.mpw_default_result_type(); this.defaultType = (defaultType != null)? defaultType: algorithm.mpw_default_result_type();
this.lastUsed = lastUsed; this.lastUsed = lastUsed;
this.hidePasswords = hidePasswords;
this.path = path; this.path = path;
this.format = format; this.format = format;
this.contentMode = contentMode; this.contentMode = contentMode;
@@ -157,6 +159,18 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
setChanged(); setChanged();
} }
public boolean isHidePasswords() {
return hidePasswords;
}
public void setHidePasswords(final boolean hidePasswords) {
if (Objects.equals( this.hidePasswords, hidePasswords ))
return;
this.hidePasswords = hidePasswords;
setChanged();
}
protected boolean isComplete() { protected boolean isComplete() {
return complete; return complete;
} }
@@ -207,6 +221,7 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
super.reset(); super.reset();
} }
@Nonnull
@Override @Override
public MPFileSite addSite(final String siteName) { public MPFileSite addSite(final String siteName) {
return addSite( new MPFileSite( this, siteName ) ); return addSite( new MPFileSite( this, siteName ) );

View File

@@ -66,6 +66,7 @@ public class MPFileUserManager {
protected MPFileUserManager(final File path) { protected MPFileUserManager(final File path) {
this.path = path; this.path = path;
reload();
} }
public void reload() { public void reload() {
@@ -128,12 +129,13 @@ public class MPFileUserManager {
return ImmutableSortedSet.copyOf( userByName.values() ); return ImmutableSortedSet.copyOf( userByName.values() );
} }
public boolean addListener(final Listener listener) { public void addListener(final Listener listener) {
return listeners.add( listener ); if (listeners.add( listener ))
listener.onFilesUpdated( getFiles() );
} }
public boolean removeListener(final Listener listener) { public void removeListener(final Listener listener) {
return listeners.remove( listener ); listeners.remove( listener );
} }
private void fireUpdated() { private void fireUpdated() {
@@ -141,7 +143,6 @@ public class MPFileUserManager {
return; return;
ImmutableSortedSet<MPFileUser> files = getFiles(); ImmutableSortedSet<MPFileUser> files = getFiles();
for (final Listener listener : listeners) for (final Listener listener : listeners)
listener.onFilesUpdated( files ); listener.onFilesUpdated( files );
} }

View File

@@ -67,7 +67,7 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
else if ((fullName != null) && (keyID != null)) else if ((fullName != null) && (keyID != null))
// Ends the header. // Ends the header.
return new MPFileUser( fullName, keyID, MPAlgorithm.Version.fromInt( mpVersion ).getAlgorithm(), return new MPFileUser( fullName, keyID, MPAlgorithm.Version.fromInt( mpVersion ).getAlgorithm(),
avatar, defaultType, new Instant( 0 ), avatar, defaultType, new Instant( 0 ), false,
clearContent? MPMarshaller.ContentMode.VISIBLE: MPMarshaller.ContentMode.PROTECTED, clearContent? MPMarshaller.ContentMode.VISIBLE: MPMarshaller.ContentMode.PROTECTED,
MPMarshalFormat.Flat, file.getParentFile() ); MPMarshalFormat.Flat, file.getParentFile() );
} }

View File

@@ -28,8 +28,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.lyndir.lhunath.opal.system.CodeUtils; import com.lyndir.lhunath.opal.system.CodeUtils;
import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPModelConstants;
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
import com.lyndir.masterpassword.model.MPModelConstants;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File; import java.io.File;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@@ -77,6 +77,7 @@ public class MPJSONFile extends MPJSONAnyObject {
user.avatar = modelUser.getAvatar(); user.avatar = modelUser.getAvatar();
user.full_name = modelUser.getFullName(); user.full_name = modelUser.getFullName();
user.last_used = MPModelConstants.dateTimeFormatter.print( modelUser.getLastUsed() ); user.last_used = MPModelConstants.dateTimeFormatter.print( modelUser.getLastUsed() );
user.hide_passwords = modelUser.isHidePasswords();
user.key_id = modelUser.exportKeyID(); user.key_id = modelUser.exportKeyID();
user.algorithm = modelUser.getAlgorithm().version(); user.algorithm = modelUser.getAlgorithm().version();
user.default_type = modelUser.getDefaultType(); user.default_type = modelUser.getDefaultType();
@@ -142,7 +143,7 @@ public class MPJSONFile extends MPJSONAnyObject {
user.full_name, CodeUtils.decodeHex( user.key_id ), algorithm, user.avatar, user.full_name, CodeUtils.decodeHex( user.key_id ), algorithm, user.avatar,
(user.default_type != null)? user.default_type: algorithm.mpw_default_result_type(), (user.default_type != null)? user.default_type: algorithm.mpw_default_result_type(),
(user.last_used != null)? MPModelConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(), (user.last_used != null)? MPModelConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(),
export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE, user.hide_passwords, export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE,
MPMarshalFormat.JSON, file.getParentFile() MPMarshalFormat.JSON, file.getParentFile()
); );
} }
@@ -170,7 +171,7 @@ public class MPJSONFile extends MPJSONAnyObject {
if (fileSite.questions != null) if (fileSite.questions != null)
for (final Map.Entry<String, Site.Question> questionEntry : fileSite.questions.entrySet()) { for (final Map.Entry<String, Site.Question> questionEntry : fileSite.questions.entrySet()) {
Site.Question fileQuestion = questionEntry.getValue(); Site.Question fileQuestion = questionEntry.getValue();
MPFileQuestion question = new MPFileQuestion( site, questionEntry.getKey(), MPFileQuestion question = new MPFileQuestion( site, ifNotNullElse( questionEntry.getKey(), "" ),
fileQuestion.type, export.redacted? fileQuestion.answer: null ); fileQuestion.type, export.redacted? fileQuestion.answer: null );
if (!export.redacted && (fileQuestion.answer != null)) if (!export.redacted && (fileQuestion.answer != null))
@@ -202,9 +203,10 @@ public class MPJSONFile extends MPJSONAnyObject {
public static class User extends MPJSONAnyObject { public static class User extends MPJSONAnyObject {
int avatar; int avatar;
String full_name; String full_name;
String last_used; String last_used;
boolean hide_passwords;
@Nullable @Nullable
String key_id; String key_id;
@Nullable @Nullable