2
0

Compare commits

..

18 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
27 changed files with 667 additions and 347 deletions

View File

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

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

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

@@ -147,6 +147,7 @@ public class MPMasterKey {
* @return {@code null} if the result type is missing a required parameter. * @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 @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,
@@ -184,6 +185,7 @@ 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 @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,

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,7 +1,7 @@
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; import javax.annotation.Nonnull;
@@ -19,4 +19,14 @@ public class MPNewSite extends MPBasicSite<MPUser<?>, MPQuestion> {
public MPQuestion addQuestion(final String keyword) { public MPQuestion addQuestion(final String keyword) {
throw new UnsupportedOperationException( "Cannot add a question to a site that hasn't been created yet." ); 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,5 +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.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;
@@ -15,6 +21,8 @@ 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 JList<E> list; private JList<E> list;
@@ -51,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();
@@ -69,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
@@ -104,11 +120,6 @@ public class CollectionListModel<E> extends AbstractListModel<E>
return selectedItem; return selectedItem;
} }
public CollectionListModel<E> select(final E selectedItem) {
setSelectedItem( selectedItem );
return this;
}
public synchronized void registerList(final JList<E> list) { public synchronized void registerList(final JList<E> list) {
// TODO: This class should probably implement ListSelectionModel instead. // TODO: This class should probably implement ListSelectionModel instead.
if (this.list != null) if (this.list != null)
@@ -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

@@ -46,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 );
@@ -243,6 +243,9 @@ public abstract class Components {
return this; return this;
} }
} ); } );
Dimension cellSize = getCellRenderer().getListCellRendererComponent( this, null, 0, false, false ).getPreferredSize();
setFixedCellWidth( cellSize.width );
setFixedCellHeight( cellSize.height );
if (model instanceof CollectionListModel) if (model instanceof CollectionListModel)
((CollectionListModel<E>) model).registerList( this ); ((CollectionListModel<E>) model).registerList( this );
@@ -423,10 +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 ) {
{ {
setBackground( null ); setBackground( null );
setAlignmentX( LEFT_ALIGNMENT ); setAlignmentX( LEFT_ALIGNMENT );
setSelected( selected );
if (selectionConsumer != null)
addItemListener( e -> selectionConsumer.accept( isSelected() ) );
} }
}; };
} }

View File

@@ -87,8 +87,11 @@ public class DocumentModel implements Selectable<String, DocumentModel> {
@Override @Override
public DocumentModel selection(@Nullable final String selectedItem, @Nullable final Consumer<String> selectionConsumer) { public DocumentModel selection(@Nullable final String selectedItem, @Nullable final Consumer<String> selectionConsumer) {
selection( selectionConsumer );
setText( selectedItem ); setText( selectedItem );
selection( selectionConsumer );
if (selectionConsumer != null)
selectionConsumer.accept( selectedItem );
return this; return this;
} }

View File

@@ -20,15 +20,12 @@ 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 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 java.util.function.Consumer;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -73,15 +70,6 @@ public abstract class Res {
return job( job, 0, TimeUnit.MILLISECONDS ); return job( job, 0, TimeUnit.MILLISECONDS );
} }
public static <V> void job(final Callable<V> job, final Consumer<V> callback) {
Futures.addCallback( job( job, 0, TimeUnit.MILLISECONDS ), new FailableCallback<V>( logger ) {
@Override
public void onSuccess(@Nullable final V result) {
callback.accept( result );
}
}, uiExecutor() );
}
public static <V> ListenableFuture<V> job(final Callable<V> job, final long delay, final TimeUnit timeUnit) { public static <V> ListenableFuture<V> job(final Callable<V> job, final long delay, final TimeUnit timeUnit) {
return jobExecutor.schedule( job, delay, timeUnit ); return jobExecutor.schedule( job, delay, timeUnit );
} }
@@ -90,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 );
} }
@@ -184,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

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

@@ -5,6 +5,7 @@ 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.*;
@@ -15,7 +16,6 @@ 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.*;
import com.lyndir.masterpassword.model.impl.*; import com.lyndir.masterpassword.model.impl.*;
import com.lyndir.masterpassword.util.Utilities;
import java.awt.*; import java.awt.*;
import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable; import java.awt.datatransfer.Transferable;
@@ -28,7 +28,7 @@ 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.*;
@@ -48,6 +48,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
private static final Logger logger = Logger.get( UserContentPanel.class ); private static final Logger logger = Logger.get( UserContentPanel.class );
private static final JButton iconButton = Components.button( Res.icons().user(), null, null ); private static final JButton iconButton = Components.button( Res.icons().user(), null, null );
private static final KeyStroke copyLoginKeyStroke = KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK ); private static final 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." );
@@ -461,7 +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 {
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." );
@@ -479,16 +480,18 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
"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; private final JLabel resultLabel;
private final JLabel passwordField; private final JLabel resultField;
private final JLabel answerLabel; private final JLabel answerLabel;
private final JLabel answerField; private final JLabel answerField;
private final JLabel queryLabel; private final JLabel queryLabel;
private final JTextField queryField; private final JTextField queryField;
private final CollectionListModel<MPSite<?>> sitesModel; private final CollectionListModel<MPQuery.Result<? extends MPSite<?>>> sitesModel;
private final JList<MPSite<?>> sitesList; 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) {
@@ -517,13 +520,14 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
answerField = Components.heading( SwingConstants.CENTER ); answerField = Components.heading( SwingConstants.CENTER );
answerField.setForeground( Res.colors().highlightFg() ); answerField.setForeground( Res.colors().highlightFg() );
answerField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) ); 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 = Components.label( SwingConstants.CENTER ) ); add( resultLabel = Components.label( SwingConstants.CENTER ) );
add( passwordField = Components.heading( SwingConstants.CENTER ) ); 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() );
@@ -538,8 +542,12 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
add( Components.strut() ); add( Components.strut() );
add( Components.scrollPane( sitesList = Components.list( add( Components.scrollPane( sitesList = Components.list(
sitesModel = new CollectionListModel<MPSite<?>>().selection( this::showSiteResult ), sitesModel = new CollectionListModel<MPQuery.Result<? extends MPSite<?>>>().selection( this::showSiteItem ),
this::getSiteDescription ) ) ); 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.strut() );
add( Components.label( strf( add( Components.label( strf(
@@ -549,27 +557,35 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
KeyEvent.getKeyText( copyLoginKeyStroke.getKeyCode() ) ) ) ); 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] ) ) ) );
@@ -580,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;
@@ -627,22 +643,22 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} }
public void showSiteQuestions() { public void showSiteQuestions() {
MPSite<?> site = sitesModel.getSelectedItem(); MPSite<?> site = getSite();
if (site == null) if (site == null)
return; return;
CollectionListModel<MPQuestion> questionsModel = new CollectionListModel<MPQuestion>().selection( this::showQuestionResult ); JList<MPQuery.Result<? extends MPQuestion>> questionsList =
JList<MPQuestion> questionsList = Components.list( Components.list( questionsModel, this::getQuestionDescription );
questionsModel, question -> Strings.isNullOrEmpty( question.getKeyword() )? "<site>": question.getKeyword() ); JTextField queryField = Components.textField( null, queryText -> Res.job( () -> {
JTextField queryField = Components.textField( null, query -> Res.job( () -> { MPQuery query = new MPQuery( queryText );
Collection<MPQuestion> questions = new LinkedList<>( site.findQuestions( query ) ); Collection<MPQuery.Result<? extends MPQuestion>> questionItems = new LinkedList<>( site.findQuestions( query ) );
if (questions.stream().noneMatch( question -> question.getKeyword().equalsIgnoreCase( query ) )) if (questionItems.stream().noneMatch( MPQuery.Result::isExact ))
questions.add( new MPNewQuestion( site, Utilities.ifNotNullElse( query, "" ) ) ); questionItems.add( MPQuery.Result.allOf( new MPNewQuestion( site, query.getQuery() ), query.getQuery() ) );
Res.ui( () -> questionsModel.set( questions ) ); Res.ui( () -> questionsModel.set( questionItems ) );
} ) ); } ) );
queryField.putClientProperty( "JTextField.variant", "search" ); queryField.putClientProperty( "JTextField.variant", "search" );
queryField.addActionListener( event -> useQuestion( questionsModel.getSelectedItem() ) ); queryField.addActionListener( this::useQuestion );
queryField.addKeyListener( new KeyAdapter() { queryField.addKeyListener( new KeyAdapter() {
@Override @Override
public void keyPressed(final KeyEvent event) { public void keyPressed(final KeyEvent event) {
@@ -672,7 +688,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} }
public void showSiteValues() { public void showSiteValues() {
MPSite<?> site = sitesModel.getSelectedItem(); MPSite<?> site = getSite();
if (site == null) if (site == null)
return; return;
@@ -717,7 +733,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} }
public void showSiteKeys() { public void showSiteKeys() {
MPSite<?> site = sitesModel.getSelectedItem(); MPSite<?> site = getSite();
if (site == null) if (site == null)
return; return;
@@ -787,7 +803,7 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} }
public void deleteSite() { public void deleteSite() {
MPSite<?> site = sitesModel.getSelectedItem(); MPSite<?> site = getSite();
if (site == null) if (site == null)
return; return;
@@ -797,61 +813,92 @@ 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 String getQuestionDescription(@Nullable final MPQuery.Result<? extends MPQuestion> item) {
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) { private void useSite(final ActionEvent event) {
MPSite<?> site = sitesModel.getSelectedItem(); 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( event );
} site = ((MPNewSite) site).addTo( user );
return;
} }
boolean loginResult = (copyLoginKeyStroke.getModifiers() & event.getModifiers()) != 0; boolean loginResult = (copyLoginKeyStroke.getModifiers() & event.getModifiers()) != 0;
showSiteResult( site, loginResult, result -> { 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();
copyResult( result ); copyResult( result );
} ); } );
} }
private void showSiteResult(@Nullable final MPSite<?> site) { private void setShowLogin(final boolean showLogin) {
showSiteResult( site, false, result -> { if (showLogin == this.showLogin)
return;
this.showLogin = showLogin;
showSiteItem( sitesModel.getSelectedItem() );
}
private void showSiteItem(@Nullable final MPQuery.Result<? extends MPSite<?>> item) {
MPSite<?> site = (item != null)? item.getOption(): null;
Res.ui( getSiteResult( site, showLogin ), result -> {
if (!showLogin && (site != null))
resultLabel.setText( (result != null)? strf( "Your password for %s:", site.getSiteName() ): " " );
else if (showLogin && (site != null))
resultLabel.setText( (result != null)? strf( "Your login for %s:", site.getSiteName() ): " " );
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 void showSiteResult(@Nullable final MPSite<?> site, final boolean loginResult, final Consumer<String> resultCallback) { private ListenableFuture<String> getSiteResult(@Nullable final MPSite<?> site, final boolean loginResult) {
Res.job( () -> { return Res.job( () -> {
try { try {
if (site != null) if (site != null)
return loginResult? site.getLogin(): site.getResult(); return loginResult? site.getLogin(): site.getResult();
@@ -861,57 +908,37 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
} }
return null; return null;
}, resultCallback.andThen( result -> Res.ui( () -> { } );
passwordLabel.setText( ((result != null) && (site != null))? strf( "Your password for %s:", site.getSiteName() ): " " );
passwordField.setText( (result != null)? result: " " );
settingsButton.setEnabled( result != null );
questionsButton.setEnabled( result != null );
editButton.setEnabled( result != null );
keyButton.setEnabled( result != null );
deleteButton.setEnabled( result != null );
} ) ) );
} }
private void useQuestion(@Nullable final MPQuestion question) { private void useQuestion(final ActionEvent event) {
MPQuestion question = getQuestion();
if (question instanceof MPNewQuestion) { if (question instanceof MPNewQuestion) {
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog( if (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(
this, this,
strf( "<html>Remember the security question with keyword <strong>%s</strong>?</html>", strf( "<html>Remember the security question with keyword <strong>%s</strong>?</html>",
Strings.isNullOrEmpty( question.getKeyword() )? "<empty>": question.getKeyword() ), Strings.isNullOrEmpty( question.getKeyword() )? "<empty>": question.getKeyword() ),
"New Question", JOptionPane.YES_NO_OPTION )) { "New Question", JOptionPane.YES_NO_OPTION ))
useQuestion( question.getSite().addQuestion( question.getKeyword() ) ); return;
}
return; question = question.getSite().addQuestion( question.getKeyword() );
} }
showQuestionResult( question, result -> { MPQuestion fquestion = question;
Res.ui( getQuestionResult( question ), result -> {
if (result == null) if (result == null)
return; return;
if (question instanceof MPFileQuestion) if (fquestion instanceof MPFileQuestion)
((MPFileQuestion) question).use(); ((MPFileQuestion) fquestion).use();
copyResult( result ); copyResult( result );
} ); } );
} }
private void showQuestionResult(@Nullable final MPQuestion question) { private void showQuestionItem(@Nullable final MPQuery.Result<? extends MPQuestion> item) {
showQuestionResult( question, answer -> { MPQuestion question = (item != null)? item.getOption(): null;
} ); Res.ui( getQuestionResult( question ), answer -> {
}
private void showQuestionResult(@Nullable final MPQuestion question, final Consumer<String> resultCallback) {
Res.job( () -> {
try {
if (question != null)
return question.getAnswer();
}
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
logger.err( e, "While resolving answer for: %s", question );
}
return null;
}, resultCallback.andThen( answer -> Res.ui( () -> {
if ((answer == null) || (question == null)) if ((answer == null) || (question == null))
answerLabel.setText( " " ); answerLabel.setText( " " );
else else
@@ -921,7 +948,21 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
strf( "<html>Answer for site <b>%s</b>, of question with keyword <b>%s</b>:", strf( "<html>Answer for site <b>%s</b>, of question with keyword <b>%s</b>:",
question.getSite().getSiteName(), question.getKeyword() ) ); question.getSite().getSiteName(), question.getKeyword() ) );
answerField.setText( (answer != null)? answer: " " ); 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) { private void copyResult(final String result) {
@@ -936,9 +977,29 @@ public class UserContentPanel extends JPanel implements MasterPassword.Listener,
Window window = SwingUtilities.windowForComponent( UserContentPanel.this ); Window window = SwingUtilities.windowForComponent( UserContentPanel.this );
if (window instanceof Frame) if (window instanceof Frame)
((Frame) window).setExtendedState( Frame.ICONIFIED ); ((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) {
} }
@@ -955,25 +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<>( user.findSites( query ) ); MPQuery query = new MPQuery( queryText );
Collection<MPQuery.Result<? extends MPSite<?>>> siteItems =
new LinkedList<>( user.findSites( query ) );
if (!Strings.isNullOrEmpty( query )) if (!Strings.isNullOrEmpty( queryText ))
if (sites.stream().noneMatch( site -> site.getSiteName().equalsIgnoreCase( query ) )) if (siteItems.stream().noneMatch( MPQuery.Result::isExact )) {
sites.add( new MPNewSite( user, query ) ); 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
@@ -983,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;
}
} }
} }

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

@@ -80,6 +80,9 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
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 @Nullable
String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext,
@Nullable UnsignedInteger counter, MPResultType type, @Nullable String state) @Nullable UnsignedInteger counter, MPResultType type, @Nullable String state)
@@ -90,13 +93,13 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
@Nullable UnsignedInteger counter, MPResultType type, String state) @Nullable UnsignedInteger counter, MPResultType type, String state)
throws MPKeyUnavailableException, MPAlgorithmException; throws MPKeyUnavailableException, MPAlgorithmException;
@Nonnull @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;
@@ -117,5 +120,5 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
Collection<Q> getQuestions(); Collection<Q> getQuestions();
@Nonnull @Nonnull
ImmutableCollection<Q> findQuestions(@Nullable String query); ImmutableCollection<MPQuery.Result<Q>> findQuestions(MPQuery query);
} }

View File

@@ -112,7 +112,7 @@ 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);
void addListener(Listener listener); void addListener(Listener listener);

View File

@@ -21,7 +21,6 @@ package com.lyndir.masterpassword.model.impl;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*; import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*; import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.ImmutableSortedSet;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
@@ -63,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() {
@@ -159,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 {
@@ -167,6 +170,14 @@ 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
public U getUser() {
return user;
}
@Nonnull @Nonnull
@Override @Override
public Q addQuestion(final Q question) { public Q addQuestion(final Q question) {
@@ -193,21 +204,14 @@ public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> ext
@Nonnull @Nonnull
@Override @Override
public ImmutableCollection<Q> findQuestions(@Nullable final String query) { public ImmutableCollection<MPQuery.Result<Q>> findQuestions(final MPQuery query) {
ImmutableSortedSet.Builder<Q> results = ImmutableSortedSet.naturalOrder(); ImmutableSortedSet.Builder<MPQuery.Result<Q>> results = ImmutableSortedSet.naturalOrder();
for (final Q question : getQuestions()) for (final Q question : questions)
if (Strings.isNullOrEmpty( query ) || question.getKeyword().startsWith( query )) query.find( question, MPQuestion::getKeyword ).ifPresent( results::add );
results.add( question );
return results.build(); return results.build();
} }
@Nonnull
@Override
public U getUser() {
return user;
}
@Override @Override
protected void onChanged() { protected void onChanged() {
super.onChanged(); super.onChanged();

View File

@@ -20,7 +20,6 @@ package com.lyndir.masterpassword.model.impl;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*; import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.ImmutableSortedSet;
import com.lyndir.lhunath.opal.system.CodeUtils; import com.lyndir.lhunath.opal.system.CodeUtils;
@@ -201,11 +200,10 @@ 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();
for (final S site : getSites()) for (final S site : sites.values())
if (Strings.isNullOrEmpty( query ) || site.getSiteName().startsWith( query )) query.find( site, MPSite::getSiteName ).ifPresent( results::add );
results.add( site );
return results.build(); return results.build();
} }

View File

@@ -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 {

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

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