2
0

Compare commits

...

61 Commits

Author SHA1 Message Date
Maarten Billemont
39f6893742 2.7-java-7 2018-08-27 18:24:46 -04:00
Maarten Billemont
7bf7b8981c Fix duplication of user names in files list. 2018-08-27 18:24:00 -04:00
Maarten Billemont
09abe21fed Release masterpassword-gui-2.7.6 2018-08-27 13:22:03 -04:00
Maarten Billemont
6fae0fe425 2.7-java-6 2018-08-27 13:11:32 -04:00
Maarten Billemont
0558176847 Fix dependencies in native mpw DLLs. 2018-08-27 13:07:38 -04:00
Maarten Billemont
c553201cda Small site update. 2018-08-26 20:41:27 -04:00
Maarten Billemont
665be9494b Release masterpassword-gui-2.7.5 2018-08-26 18:19:01 -04:00
Maarten Billemont
5ca81b4aa7 Don't use daemon when setting release passwords in environment. 2018-08-25 11:57:36 -04:00
Maarten Billemont
3cbb063926 Refactor Native to try and load other architectures. 2018-08-24 16:33:14 -04:00
Maarten Billemont
d5551c8c8c Key calculator and access to the full algorithm. 2018-08-24 13:48:53 -04:00
Maarten Billemont
9a40e52d53 2.7-java-5 2018-08-19 16:13:39 -04:00
Maarten Billemont
6f0d768e69 Saving custom passwords and logins. 2018-08-19 16:11:43 -04:00
Maarten Billemont
40fdc8d248 Fix initialization dependency cycle & load files on init. 2018-08-18 13:43:41 -04:00
Maarten Billemont
6b9e1b8cb8 Standard label font & fix warnings. 2018-08-14 12:06:04 -04:00
Maarten Billemont
f41cdb8742 Site security questions and copy login name. 2018-08-13 17:53:31 -04:00
Maarten Billemont
10c6d203b8 Implement security answers & immediate site lookup. 2018-08-07 00:07:16 -04:00
Maarten Billemont
7d1aa9c9f4 Release 2.7.4 2018-08-02 12:34:24 -04:00
Maarten Billemont
c26281e3b7 2.7-java-4 2018-08-02 12:20:45 -04:00
Maarten Billemont
f0b1f0c9e0 Build for older glibc. 2018-08-02 12:19:49 -04:00
Maarten Billemont
9682efc7c9 Release masterpassword-gui-2.7.3 2018-08-02 01:44:50 -04:00
Maarten Billemont
1264cad377 2.7-java-3 2018-08-02 01:37:37 -04:00
Maarten Billemont
d185a0af14 Add mpw native binary for windows 32-bit. 2018-08-02 01:37:10 -04:00
Maarten Billemont
4275a6cc61 Fix build on Windows. 2018-08-02 01:32:55 -04:00
Maarten Billemont
c94ff429e8 Switch linux build of libmpw to debian for glibc instead of musl libc. 2018-08-01 20:13:42 -04:00
Maarten Billemont
00744cb264 Statically link the mpw library. 2018-08-01 14:20:47 -04:00
Maarten Billemont
7202fe6d1d Bump site for release of masterpassword-gui-2.7.2.jar 2018-07-31 15:35:57 -04:00
Maarten Billemont
63b4d9cd2e 2.7-java-2 2018-07-31 15:32:13 -04:00
Maarten Billemont
36a7c7f423 Clean up iconifying on copy. 2018-07-31 15:31:47 -04:00
Maarten Billemont
c2c4fb18bf Help improvements. 2018-07-31 15:16:33 -04:00
Maarten Billemont
3fc8acba70 Global hotkey, iconifying and application activation, help text. 2018-07-31 14:55:19 -04:00
Maarten Billemont
f5c0c4d787 Fix offsetting local time back to UTC. 2018-07-31 12:44:49 -04:00
Maarten Billemont
86775f1c75 Standardize epoch time calculation. 2018-07-31 09:27:41 -04:00
Maarten Billemont
2bb190f49a Bump site for masterpassword-gui-2.7.1 release. 2018-07-29 15:38:54 -04:00
Maarten Billemont
77c4a2af46 2.7-java-1 2018-07-29 15:29:19 -04:00
Maarten Billemont
3da82d30b1 Add support for creating incognito users. 2018-07-29 15:26:48 -04:00
Maarten Billemont
97532fdce6 Print UI dates in current time zone. 2018-07-29 15:18:54 -04:00
Maarten Billemont
fe63a2756a Fix default type for new sites & site UI updating. 2018-07-29 15:10:45 -04:00
Maarten Billemont
928b617ed0 Import & export users + improved user state tracking. 2018-07-29 14:01:07 -04:00
Maarten Billemont
18ecc41b39 Fix issues with UnsignedIntegerModel in a spinner. 2018-07-29 01:38:58 -04:00
Maarten Billemont
a6e9e89ace Update UI instead of resetting when site is updated. 2018-07-29 01:38:15 -04:00
Maarten Billemont
0b7494ecbf We use JDK9 APIs now. 2018-07-29 00:19:05 -04:00
Maarten Billemont
8377c9c615 JDK9 platform-independent way of managing application events. 2018-07-29 00:08:09 -04:00
Maarten Billemont
37a7cfa530 Support resetting user's master password. 2018-07-28 21:53:08 -04:00
Maarten Billemont
978b758079 New user fixes. 2018-07-28 19:56:20 -04:00
Maarten Billemont
38f09021b3 Button tooltips and improvements. 2018-07-28 18:11:36 -04:00
Maarten Billemont
7455fba55e Adding and deleting users and sites. 2018-07-28 17:52:43 -04:00
Maarten Billemont
8cd9755616 Update GradientPanel's opaque properly. 2018-07-28 14:30:36 -04:00
Maarten Billemont
46d301df94 Site settings & add sites. 2018-07-28 14:03:49 -04:00
Maarten Billemont
e639137304 Avatar configuration & move preferences into user panel. 2018-07-26 15:07:37 -04:00
Maarten Billemont
7c83a62f91 Support for building with JDK 10. 2018-07-26 15:07:17 -04:00
Maarten Billemont
513840e2c4 Read in site questions from json & don't serialize incomplete MPFileUser 2018-07-23 23:59:11 -04:00
Maarten Billemont
8f7faa9e4e User preferences. 2018-07-23 23:34:32 -04:00
Maarten Billemont
16cdcda94b Identicon support and UI improvements. 2018-07-23 11:23:26 -04:00
Maarten Billemont
400ebe59db Implement sites list and copy result. 2018-07-19 13:56:26 -04:00
Maarten Billemont
476a4046e7 Use standard control highlight color for password. 2018-07-18 17:02:51 -04:00
Maarten Billemont
3403449ca2 Cleanup and fix some warnings. 2018-07-18 15:46:06 -04:00
Maarten Billemont
596ace51ea WIP - new Java UI. 2018-07-18 12:27:19 -04:00
Maarten Billemont
80b5fcd785 Refactor model, improved isolation & access unauthenticated file metadata. 2018-07-18 12:23:53 -04:00
Maarten Billemont
a16bc9a318 Don't save changes made to model while it's being read from file. 2018-07-10 00:54:11 -04:00
Maarten Billemont
462dd4e89b Prepare for 2.7 release. 2018-07-10 00:39:52 -04:00
Maarten Billemont
e5ff374a9c Better way of checking implementation-version. 2018-07-10 00:39:35 -04:00
110 changed files with 3881 additions and 2271 deletions

1
.gitignore vendored
View File

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

View File

@@ -14,5 +14,5 @@ build_project:
- "( xcodebuild -workspace platform-darwin/MasterPassword.xcworkspace -configuration 'Test' -scheme 'MasterPassword macOS' clean build )"
tags:
- brew
- java
- java_9
- xcode_9

View File

@@ -1,9 +1,12 @@
FROM alpine
FROM debian:stable-slim
# For i386
#FROM i386/alpine
#FROM i386/debian:stable-slim
#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 cd /mpw/gradle && ./gradlew -i clean build

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

@@ -5,26 +5,36 @@
<option name="myDefaultNotNull" value="javax.annotation.Nonnull" />
<option name="myNullables">
<value>
<list size="4">
<list size="9">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="4" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="4">
<list size="9">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="javax.validation.constraints.NotNull" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="10" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
org.gradle.daemon=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx1536M
android.enableD8.desugaring=true

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="10117"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
<capability name="box content view" minToolsVersion="7.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
<capability name="stacking Non-gravity area distributions on NSStackView" minToolsVersion="7.0" minSystemVersion="10.11"/>
</dependencies>
<objects>
@@ -27,7 +28,7 @@
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Master Password" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MPSitesWindow">
<windowStyleMask key="styleMask" texturedBackground="YES" unifiedTitleAndToolbar="YES" fullSizeContentView="YES"/>
<windowStyleMask key="styleMask" texturedBackground="YES" fullSizeContentView="YES"/>
<windowCollectionBehavior key="collectionBehavior" moveToActiveSpace="YES" transient="YES" ignoresCycle="YES" fullScreenAuxiliary="YES"/>
<rect key="contentRect" x="0.0" y="0.0" width="640" height="577"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
@@ -35,7 +36,7 @@
<rect key="frame" x="0.0" y="0.0" width="640" height="577"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView blendingMode="behindWindow" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="eRe-Ef-AZx">
<visualEffectView blendingMode="behindWindow" material="appearanceBased" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="eRe-Ef-AZx">
<rect key="frame" x="0.0" y="0.0" width="640" height="577"/>
</visualEffectView>
<progressIndicator hidden="YES" wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="oSh-Ec-8Nf" userLabel="Progress Spinner">
@@ -45,7 +46,7 @@
<rect key="frame" x="20" y="383" width="600" height="150"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ond-dT-x5d" userLabel="Site Password Label">
<rect key="frame" x="157" y="116" width="285" height="14"/>
<rect key="frame" x="157" y="116" width="286" height="14"/>
<shadow key="shadow" blurRadius="0.5">
<size key="offset" width="0.0" height="1"/>
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
@@ -73,7 +74,7 @@
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ia6-7b-dFr">
<rect key="frame" x="115" y="68" width="370" height="14"/>
<rect key="frame" x="127" y="68" width="347" height="14"/>
<shadow key="shadow" blurRadius="0.5">
<size key="offset" width="0.0" height="1"/>
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
@@ -186,7 +187,7 @@
</contentFilters>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="KeljXoleKowi9@" placeholderString="" id="WVV-EE-tkB">
<font key="font" size="64" name="SourceCodePro-Regular"/>
<color key="textColor" name="keyboardFocusIndicatorColor" catalog="System" colorSpace="catalog"/>
<color key="textColor" name="selectedControlColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
@@ -223,13 +224,13 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnReordering="NO" columnResizing="NO" multipleSelection="NO" emptySelection="NO" autosaveColumns="NO" rowHeight="33" rowSizeStyle="automatic" viewBased="YES" floatsGroupRows="NO" id="xvJ-5c-vDp" customClass="MPSitesTableView">
<rect key="frame" x="0.0" y="0.0" width="515" height="0.0"/>
<rect key="frame" x="0.0" y="0.0" width="515" height="180"/>
<autoresizingMask key="autoresizingMask"/>
<size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" white="1" alpha="0.0" colorSpace="deviceWhite"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn editable="NO" width="512" minWidth="512" maxWidth="512" id="S71-gk-yF7">
<tableColumn identifier="" editable="NO" width="512" minWidth="512" maxWidth="512" id="S71-gk-yF7">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
@@ -312,7 +313,7 @@
</connections>
</scrollView>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="nM8-O3-spM" customClass="MPGradientView">
<rect key="frame" x="0.0" y="0.0" width="640" height="212"/>
<rect key="frame" x="0.0" y="0.0" width="640" height="214"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="color" keyPath="startingColor">
<color key="value" red="0.7019608021" green="0.7019608021" blue="0.7019608021" alpha="0.0" colorSpace="calibratedRGB"/>
@@ -570,7 +571,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
</connections>
</textField>
<stackView distribution="fill" orientation="horizontal" alignment="bottom" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pHt-gg-ZNX">
<rect key="frame" x="72" y="20" width="495" height="152"/>
<rect key="frame" x="72" y="20" width="495" height="154"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1Qo-iG-CQt">
<rect key="frame" x="0.0" y="-1" width="85" height="19"/>
@@ -593,10 +594,10 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
</connections>
</button>
<stackView distribution="fill" orientation="vertical" alignment="centerX" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DT0-RU-3LT">
<rect key="frame" x="93" y="0.0" width="177" height="152"/>
<rect key="frame" x="93" y="0.0" width="177" height="154"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="uol-dE-I8H">
<rect key="frame" x="77" y="138" width="23" height="14"/>
<rect key="frame" x="77" y="140" width="23" height="14"/>
<shadow key="shadow" blurRadius="0.5">
<size key="offset" width="0.0" height="1"/>
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
@@ -633,7 +634,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
</connections>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="brI-fg-Kav">
<rect key="frame" x="40" y="111" width="96" height="19"/>
<rect key="frame" x="41" y="113" width="96" height="19"/>
<shadow key="shadow">
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
</shadow>
@@ -662,7 +663,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="R46-fx-n14">
<rect key="frame" x="0.0" y="85" width="177" height="19"/>
<rect key="frame" x="0.0" y="87" width="177" height="19"/>
<shadow key="shadow">
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
</shadow>
@@ -688,10 +689,10 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
</connections>
</button>
<stackView distribution="fill" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Bgn-Ne-fQ7" userLabel="Version Container">
<rect key="frame" x="70" y="56" width="36" height="22"/>
<rect key="frame" x="71" y="57" width="36" height="23"/>
<subviews>
<stepper horizontalHuggingPriority="750" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mcq-qD-yte">
<rect key="frame" x="-3" y="-3" width="19" height="27"/>
<rect key="frame" x="-3" y="-2" width="19" height="27"/>
<shadow key="shadow">
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
</shadow>
@@ -701,7 +702,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
</connections>
</stepper>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gyg-Fh-yn7">
<rect key="frame" x="19" y="3" width="19" height="19"/>
<rect key="frame" x="19" y="4" width="19" height="19"/>
<shadow key="shadow" blurRadius="0.5">
<size key="offset" width="0.0" height="1"/>
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
@@ -742,10 +743,10 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
</connections>
</stackView>
<stackView distribution="fill" orientation="horizontal" alignment="centerY" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6II-KA-cNi" userLabel="Counter Container">
<rect key="frame" x="74" y="26" width="28" height="22"/>
<rect key="frame" x="75" y="26" width="28" height="23"/>
<subviews>
<stepper horizontalHuggingPriority="750" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="XgA-Vl-CKh" userLabel="Counter Stepper">
<rect key="frame" x="-3" y="-3" width="19" height="27"/>
<rect key="frame" x="-3" y="-2" width="19" height="27"/>
<shadow key="shadow">
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
</shadow>
@@ -755,7 +756,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
</connections>
</stepper>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="NvO-kt-eZ2" userLabel="Counter Field">
<rect key="frame" x="19" y="1" width="11" height="19"/>
<rect key="frame" x="19" y="2" width="11" height="19"/>
<shadow key="shadow" blurRadius="0.5">
<size key="offset" width="0.0" height="1"/>
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
@@ -888,7 +889,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
</customSpacing>
</stackView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="luC-0j-BeV">
<rect key="frame" x="134" y="50" width="103" height="14"/>
<rect key="frame" x="135" y="51" width="103" height="14"/>
<shadow key="shadow" blurRadius="0.5">
<size key="offset" width="0.0" height="1"/>
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
@@ -925,7 +926,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gjx-bt-fKM">
<rect key="frame" x="133" y="80" width="100" height="14"/>
<rect key="frame" x="134" y="82" width="100" height="14"/>
<shadow key="shadow" blurRadius="0.5">
<size key="offset" width="0.0" height="1"/>
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
@@ -962,7 +963,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
</connections>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="dbM-ja-dKO" userLabel="Version Tip">
<rect key="frame" x="87" y="106" width="332" height="14"/>
<rect key="frame" x="88" y="108" width="332" height="14"/>
<shadow key="shadow" blurRadius="0.5">
<size key="offset" width="0.0" height="1"/>
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
@@ -1170,7 +1171,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
<binding destination="-2" name="contentArray" keyPath="sites" id="c96-Dv-HK1"/>
</connections>
</arrayController>
<box autoresizesSubviews="NO" title="Choose a password type for apple.com:" borderType="line" id="bZe-7q-i6q">
<box autoresizesSubviews="NO" borderType="line" title="Choose a password type for apple.com:" id="bZe-7q-i6q">
<rect key="frame" x="0.0" y="0.0" width="416" height="296"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<view key="contentView" id="hAc-y9-IMT">
@@ -1243,11 +1244,9 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
<constraint firstItem="3fr-Fd-pxx" firstAttribute="top" secondItem="hAc-y9-IMT" secondAttribute="top" constant="8" id="xVT-HC-qsE"/>
</constraints>
</view>
<color key="borderColor" white="0.0" alpha="0.41999999999999998" colorSpace="calibratedWhite"/>
<color key="fillColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<point key="canvasLocation" x="333" y="125"/>
</box>
<box autoresizesSubviews="NO" title="Answer to security questions for apple.com:" borderType="line" id="hi3-SX-Td3">
<box autoresizesSubviews="NO" borderType="line" title="Answer to security questions for apple.com:" id="hi3-SX-Td3">
<rect key="frame" x="0.0" y="0.0" width="416" height="180"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<view key="contentView" wantsLayer="YES" id="8Ep-zH-Nzv">
@@ -1321,8 +1320,6 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
<constraint firstAttribute="trailing" secondItem="12d-V9-LDB" secondAttribute="trailing" constant="12" id="Ysu-F5-ukt"/>
<constraint firstItem="12d-V9-LDB" firstAttribute="leading" secondItem="hi3-SX-Td3" secondAttribute="leading" constant="12" id="oPv-4N-T9I"/>
</constraints>
<color key="borderColor" white="0.0" alpha="0.41999999999999998" colorSpace="calibratedWhite"/>
<color key="fillColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<point key="canvasLocation" x="333" y="491"/>
</box>
</objects>

View File

@@ -14,11 +14,10 @@ artifacts {
from 'lib'
components.withType( ComponentWithRuntimeFile ) {
if (isOptimized()) {
if (optimized)
from runtimeFile, {
into standardOperatingSystem( targetPlatform ) + '/' + standardArchitecture( targetPlatform )
}
}
}
}
}
@@ -36,7 +35,7 @@ library {
}
withType( GccCompatibleToolChain ) {
eachPlatform {
cppCompiler.withArguments { addAll( ['-x', 'c', '-O3', '-std=c11', '-Werror', '-DMPW_SODIUM=1'] ) }
cppCompiler.withArguments { addAll( ['-x', 'c', '-O3', '-Werror', '-DMPW_SODIUM=1'] ) }
}
}
}
@@ -55,27 +54,29 @@ library {
from files( new File( Jvm.current().javaHome, 'include' ) ) { first().eachDir { from it } }
}
binaries.configureEach {
def system = standardOperatingSystem( targetPlatform )
binaries.whenElementFinalized {
project.dependencies {
add( linkLibraries.name,
fileTree( "$rootDir/../lib/libsodium/build-${system}~/out/lib" ) )
}
def system = standardOperatingSystem( targetPlatform )
archive.dependsOn project.tasks.maybeCreate( "build_libsodium-${system}", Exec.class ).configure {
commandLine 'bash', "$rootDir/../lib/bin/build_libsodium-${system}"
privateHeaders.from "$rootDir/../lib/libsodium/build-${system}~/out/include"
}
clean.dependsOn project.tasks.maybeCreate( "clean_libsodium-${system}", Exec.class ).configure {
commandLine 'bash', "$rootDir/../lib/bin/build_libsodium-${system}", 'clean'
}
archive.dependsOn project.tasks.maybeCreate( "build_libjson-c-${system}", Exec.class ).configure {
commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-${system}"
privateHeaders.from "$rootDir/../lib/libjson-c/build-${system}~/out/include"
}
clean.dependsOn project.tasks.maybeCreate( "clean_libjson-c-${system}", Exec.class ).configure {
commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-${system}", 'clean'
// libsodium
archive.dependsOn project.tasks.maybeCreate( "build_libsodium-${system}", Exec ).configure {
commandLine 'bash', "$rootDir/../lib/bin/build_libsodium-${system}"
privateHeaders.from "$rootDir/../lib/libsodium/build-${system}~/out/include"
add( linkLibraries.name, fileTree( "$rootDir/../lib/libsodium/build-${system}~/out/lib" ) )
}
clean.dependsOn project.tasks.maybeCreate( "clean_libsodium-${system}", Exec ).configure {
commandLine 'bash', "$rootDir/../lib/bin/build_libsodium-${system}", 'clean'
}
// libjson-c
/*archive.dependsOn project.tasks.maybeCreate( "build_libjson-c-${system}", Exec ).configure {
commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-${system}"
privateHeaders.from "$rootDir/../lib/libjson-c/build-${system}~/out/include"
add( linkLibraries.name, fileTree( "$rootDir/../lib/libjson-c/build-${system}~/out/lib" ) )
}
clean.dependsOn project.tasks.maybeCreate( "clean_libjson-c-${system}", Exec ).configure {
commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-${system}", 'clean'
}*/
}
}
}

Binary file not shown.

View File

@@ -1,12 +1,13 @@
#include <string.h>
#include "mpw-jni.h"
#include "java/com_lyndir_masterpassword_impl_MPAlgorithmV0.h"
#include "mpw-algorithm.h"
#include "mpw-util.h"
// TODO: We may need to zero the jbytes safely.
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
if ((*vm)->GetEnv( vm, (void **)&env, JNI_VERSION_1_6 ) != JNI_OK)
return -1;

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,11 @@ plugins {
description = 'Master Password Algorithm Implementation'
tasks.withType( JavaCompile ) {
// Native headers
options.compilerArgs += ["-h", new File( new File( project.project( ':masterpassword-core' ).projectDir, 'src' ), 'java' ).absolutePath]
}
configurations {
lib
}
@@ -24,17 +29,7 @@ processResources {
into new File( processResources.outputs.files.singleFile, "lib" )
dependsOn configurations.lib {
files.each { libFile ->
from( zipTree( libFile ) )
}
files.each { libFile -> from( zipTree( libFile ) ) }
}
} )
}
compileJava {
doLast {
ant.javah( class: 'com.lyndir.masterpassword.impl.MPAlgorithmV0',
outputFile: new File( project( ':masterpassword-core' ).projectDir, 'src/mpw-jni.h' ),
classpath: files( sourceSets.main.compileClasspath, sourceSets.main.output ).asPath )
}
}

View File

@@ -28,6 +28,7 @@ import com.lyndir.lhunath.opal.system.MessageDigests;
import com.lyndir.masterpassword.impl.*;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -88,46 +89,55 @@ public abstract class MPAlgorithm {
/**
* The linear version identifier of this algorithm's implementation.
*/
@Nonnull
public abstract Version version();
/**
* mpw: defaults: initial counter value.
*/
@Nonnull
public abstract UnsignedInteger mpw_default_counter();
/**
* mpw: defaults: password result type.
*/
@Nonnull
public abstract MPResultType mpw_default_result_type();
/**
* mpw: defaults: login result type.
*/
@Nonnull
public abstract MPResultType mpw_default_login_type();
/**
* mpw: defaults: answer result type.
*/
@Nonnull
public abstract MPResultType mpw_default_answer_type();
/**
* mpw: Input character encoding.
*/
@Nonnull
public abstract Charset mpw_charset();
/**
* mpw: Platform-agnostic byte order.
*/
@Nonnull
public abstract ByteOrder mpw_byteOrder();
/**
* mpw: Key ID hash.
*/
@Nonnull
public abstract MessageDigests mpw_hash();
/**
* mpw: Site digest.
*/
@Nonnull
public abstract MessageAuthenticationDigests mpw_digest();
/**
@@ -167,12 +177,16 @@ public abstract class MPAlgorithm {
// Utilities
@Nonnull
protected abstract byte[] toBytes(int number);
@Nonnull
protected abstract byte[] toBytes(UnsignedInteger number);
@Nonnull
protected abstract byte[] toBytes(char[] characters);
@Nonnull
protected abstract byte[] toID(byte[] bytes);
@Override

View File

@@ -27,6 +27,7 @@ import com.lyndir.lhunath.opal.system.logging.Logger;
import java.nio.*;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Locale;
/**
@@ -83,6 +84,10 @@ public class MPIdenticon {
return text;
}
public String getHTML() {
return strf( "<span style='color: %s'>%s</span>", color.getCSS(), text );
}
public Color getColor() {
return color;
}
@@ -94,6 +99,15 @@ public class MPIdenticon {
BLUE,
MAGENTA,
CYAN,
MONO
MONO {
@Override
public String getCSS() {
return "inherit";
}
};
public String getCSS() {
return name().toLowerCase( Locale.ROOT );
}
}
}

View File

@@ -44,7 +44,8 @@ public class MPMasterKey {
/**
* @param masterPassword The characters of the user's master password.
* <b>Note: this method destroys the contents of the array.</b>
*
* @apiNote This method destroys the contents of the {@code masterPassword} array.
*/
@SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
public MPMasterKey(final String fullName, final char[] masterPassword) {
@@ -65,6 +66,7 @@ public class MPMasterKey {
*
* @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object.
*/
@Nonnull
public byte[] getKeyID(final MPAlgorithm algorithm)
throws MPKeyUnavailableException, MPAlgorithmException {
@@ -82,11 +84,16 @@ public class MPMasterKey {
Arrays.fill( masterPassword, (char) 0 );
}
public boolean isValid() {
return !invalidated;
}
@Nonnull
private byte[] masterKey(final MPAlgorithm algorithm)
throws MPKeyUnavailableException, MPAlgorithmException {
Preconditions.checkArgument( masterPassword.length > 0 );
if (invalidated)
if (!isValid())
throw new MPKeyUnavailableException( "Master key was invalidated." );
byte[] masterKey = keyByVersion.get( algorithm.version() );
@@ -104,6 +111,7 @@ public class MPMasterKey {
return masterKey;
}
@Nonnull
private byte[] siteKey(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter,
final MPKeyPurpose keyPurpose, @Nullable final String keyContext)
throws MPKeyUnavailableException, MPAlgorithmException {
@@ -136,13 +144,19 @@ public class MPMasterKey {
* In the case of {@link MPResultTypeClass#Stateful} types, the result of
* {@link #siteState(String, MPAlgorithm, UnsignedInteger, MPKeyPurpose, String, MPResultType, String)}.
*
* @return {@code null} if the result type is missing a required parameter.
*
* @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object.
*/
@Nullable
public String siteResult(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter,
final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
final MPResultType resultType, @Nullable final String resultParam)
throws MPKeyUnavailableException, MPAlgorithmException {
if ((resultType.getTypeClass() == MPResultTypeClass.Stateful) && (resultParam == null))
return null;
byte[] masterKey = masterKey( algorithm );
byte[] siteKey = siteKey( siteName, algorithm, siteCounter, keyPurpose, keyContext );
@@ -171,6 +185,7 @@ public class MPMasterKey {
*
* @throws MPKeyUnavailableException {@link #invalidate()} has been called on this object.
*/
@Nonnull
public String siteState(final String siteName, final MPAlgorithm algorithm, final UnsignedInteger siteCounter,
final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
final MPResultType resultType, final String resultParam)

View File

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

View File

@@ -27,6 +27,7 @@ import com.lyndir.masterpassword.*;
import java.nio.*;
import java.nio.charset.*;
import java.util.Arrays;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -122,46 +123,55 @@ public class MPAlgorithmV0 extends MPAlgorithm {
// Configuration
@Nonnull
@Override
public Version version() {
return MPAlgorithm.Version.V0;
}
@Nonnull
@Override
public UnsignedInteger mpw_default_counter() {
return UnsignedInteger.ONE;
}
@Nonnull
@Override
public MPResultType mpw_default_result_type() {
return MPResultType.GeneratedLong;
}
@Nonnull
@Override
public MPResultType mpw_default_login_type() {
return MPResultType.GeneratedName;
}
@Nonnull
@Override
public MPResultType mpw_default_answer_type() {
return MPResultType.GeneratedPhrase;
}
@Nonnull
@Override
public Charset mpw_charset() {
return Charsets.UTF_8;
}
@Nonnull
@Override
public ByteOrder mpw_byteOrder() {
return ByteOrder.BIG_ENDIAN;
}
@Nonnull
@Override
public MessageDigests mpw_hash() {
return MessageDigests.SHA256;
}
@Nonnull
@Override
public MessageAuthenticationDigests mpw_digest() {
return MessageAuthenticationDigests.HmacSHA256;
@@ -211,16 +221,19 @@ public class MPAlgorithmV0 extends MPAlgorithm {
// Utilities
@Nonnull
@Override
public byte[] toBytes(final int number) {
return ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( mpw_byteOrder() ).putInt( number ).array();
}
@Nonnull
@Override
public byte[] toBytes(final UnsignedInteger number) {
return ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( mpw_byteOrder() ).putInt( number.intValue() ).array();
}
@Nonnull
@Override
public byte[] toBytes(final char[] characters) {
ByteBuffer byteBuffer = mpw_charset().encode( CharBuffer.wrap( characters ) );
@@ -232,6 +245,7 @@ public class MPAlgorithmV0 extends MPAlgorithm {
return bytes;
}
@Nonnull
@Override
public byte[] toID(final byte[] bytes) {
return mpw_hash().of( bytes );

View File

@@ -19,6 +19,7 @@
package com.lyndir.masterpassword.impl;
import com.lyndir.masterpassword.*;
import javax.annotation.Nonnull;
/**
@@ -29,6 +30,7 @@ public class MPAlgorithmV1 extends MPAlgorithmV0 {
// Configuration
@Nonnull
@Override
public Version version() {
return MPAlgorithm.Version.V1;

View File

@@ -19,6 +19,7 @@
package com.lyndir.masterpassword.impl;
import com.lyndir.masterpassword.MPAlgorithm;
import javax.annotation.Nonnull;
/**
@@ -29,6 +30,7 @@ public class MPAlgorithmV2 extends MPAlgorithmV1 {
// Configuration
@Nonnull
@Override
public Version version() {
return MPAlgorithm.Version.V2;

View File

@@ -19,6 +19,7 @@
package com.lyndir.masterpassword.impl;
import com.lyndir.masterpassword.MPAlgorithm;
import javax.annotation.Nonnull;
/**
@@ -29,6 +30,7 @@ public class MPAlgorithmV3 extends MPAlgorithmV2 {
// Configuration
@Nonnull
@Override
public Version version() {
return MPAlgorithm.Version.V3;

View File

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

View File

@@ -0,0 +1,42 @@
package com.lyndir.masterpassword.util;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* @author lhunath, 2018-07-25
*/
public final class Utilities {
@Nullable
public static <T, R> R ifNotNull(@Nullable final T value, final Function<T, R> consumer) {
if (value == null)
return null;
return consumer.apply( value );
}
@Nonnull
public static <T> T ifNotNullElse(@Nullable final T value, @Nonnull final T nullValue) {
if (value == null)
return nullValue;
return value;
}
@Nonnull
public static <T, R> R ifNotNullElse(@Nullable final T value, final Function<T, R> consumer, @Nonnull final R nullValue) {
if (value == null)
return nullValue;
return consumer.apply( value );
}
public static <T> void ifNotNullDo(@Nullable final T value, final Consumer<T> consumer) {
if (value != null)
consumer.accept( value );
}
}

View File

@@ -0,0 +1,7 @@
/**
* @author lhunath, 2018-07-25
*/
@ParametersAreNonnullByDefault
package com.lyndir.masterpassword.util;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@@ -5,32 +5,37 @@ plugins {
}
description = 'Master Password GUI'
mainClassName = 'com.lyndir.masterpassword.gui.GUI'
mainClassName = 'com.lyndir.masterpassword.gui.MasterPassword'
dependencies {
implementation group: 'com.lyndir.lhunath.opal', name: 'opal-system', version: '1.7-p2'
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.1.2'
implementation group: 'com.yuvimasory', name: 'orange-extensions', version: '1.3.0'
implementation group: 'com.github.tulskiy', name: 'jkeymaster', version: '1.2'
compile project( ':masterpassword-model' )
}
// release with: STORE_PW=$(mpw masterpassword.keystore) KEY_PW_ANDROID=$(mpw masterpassword-android) gradle masterpassword-gui:shadowJar
shadowJar.doLast {
if (System.getenv( 'KEY_PW_DESKTOP' ) != null)
ant.signjar(
jar: archivePath,
alias: 'masterpassword-desktop',
keystore: 'masterpassword.keystore',
storepass: System.getenv( 'STORE_PW' ),
keypass: System.getenv( 'KEY_PW_DESKTOP' ),
preservelastmodified: 'true',
destdir: '.'
)
// release with: STORE_PW=$(mpw masterpassword.keystore) KEY_PW_DESKTOP=$(mpw masterpassword-desktop) gradle clean masterpassword-gui:shadowJar
shadowJar {
manifest {
attributes 'Implementation-Title': description
attributes 'Implementation-Version': version
}
doLast {
if (System.getenv( 'KEY_PW_DESKTOP' ) != null)
ant.signjar( jar: archivePath,
alias: 'masterpassword-desktop',
keystore: 'masterpassword.keystore',
storepass: System.getenv( 'STORE_PW' ),
keypass: System.getenv( 'KEY_PW_DESKTOP' ),
preservelastmodified: 'true',
destdir: '.' )
}
}
run {
// I don't fully understand why this is necessary, but without it -Dmp.log.level is lost.
systemProperties = System.properties
//systemProperties = System.properties
}

View File

@@ -1,143 +0,0 @@
//==============================================================================
// This file is part of Master Password.
// Copyright (c) 2011-2017, Maarten Billemont.
//
// Master Password is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Master Password is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You can find a copy of the GNU General Public License in the
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
//==============================================================================
package com.lyndir.masterpassword.gui;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.base.Charsets;
import com.google.common.io.ByteSource;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.TypeUtils;
import com.lyndir.masterpassword.gui.view.PasswordFrame;
import com.lyndir.masterpassword.gui.view.UnlockFrame;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.*;
import java.util.Enumeration;
import java.util.Optional;
import java.util.jar.*;
import javax.swing.*;
/**
* <p> <i>Jun 10, 2008</i> </p>
*
* @author mbillemo
*/
public class GUI implements UnlockFrame.SignInCallback {
@SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( GUI.class );
private final UnlockFrame unlockFrame = new UnlockFrame( this );
private PasswordFrame<?, ?> passwordFrame;
public static void main(final String... args) {
Thread.setDefaultUncaughtExceptionHandler(
(t, e) -> logger.err( e, "Uncaught: %s", e.getLocalizedMessage() ) );
if (Config.get().checkForUpdates())
checkUpdate();
// Try and set the system look & feel, if available.
try {
UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
}
catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
}
create().open();
}
private static GUI create() {
try {
// AppleGUI adds support for macOS features.
Optional<Class<GUI>> appleGUI = TypeUtils.loadClass( "com.lyndir.masterpassword.gui.platform.mac.AppleGUI" );
if (appleGUI.isPresent())
return appleGUI.get().getConstructor().newInstance();
}
catch (@SuppressWarnings("ErrorNotRethrown") final LinkageError ignored) {
}
catch (final IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
throw logger.bug( e );
}
// Use platform-independent GUI.
return new GUI();
}
private static void checkUpdate() {
try {
Enumeration<URL> manifestURLs = Thread.currentThread().getContextClassLoader().getResources( JarFile.MANIFEST_NAME );
while (manifestURLs.hasMoreElements())
try (InputStream manifestStream = manifestURLs.nextElement().openStream()) {
Attributes attributes = new Manifest( manifestStream ).getMainAttributes();
if (!GUI.class.getCanonicalName().equals( attributes.getValue( Attributes.Name.MAIN_CLASS ) ))
continue;
String manifestRevision = attributes.getValue( Attributes.Name.IMPLEMENTATION_VERSION );
String upstreamRevision = new ByteSource() {
@Override
public InputStream openStream()
throws IOException {
URL url = URI.create( "https://masterpassword.app/masterpassword-gui.jar.rev" ).toURL();
URLConnection conn = url.openConnection();
conn.addRequestProperty( "User-Agent", "masterpassword-gui" );
return conn.getInputStream();
}
}.asCharSource( Charsets.UTF_8 ).readFirstLine();
if ((manifestRevision != null) && (upstreamRevision != null) && !manifestRevision.equalsIgnoreCase(
upstreamRevision )) {
logger.inf( "Local Revision: <%s>", manifestRevision );
logger.inf( "Upstream Revision: <%s>", upstreamRevision );
logger.wrn( "You are not running the current official version. Please update from:%n%s",
"https://masterpassword.app/masterpassword-gui.jar" );
JOptionPane.showMessageDialog( null,
strf( "A new version of Master Password is available.%n "
+ "Please download the latest version from %s",
"https://masterpassword.app" ),
"Update Available", JOptionPane.WARNING_MESSAGE );
}
}
catch (final IOException e) {
logger.wrn( e, "Couldn't check for version update." );
}
}
catch (final IOException e) {
logger.wrn( e, "Couldn't inspect JAR." );
}
}
protected void open() {
SwingUtilities.invokeLater( () -> {
if (passwordFrame == null)
unlockFrame.setVisible( true );
else
passwordFrame.setVisible( true );
} );
}
@Override
public void signedIn(final PasswordFrame<?, ?> passwordFrame) {
this.passwordFrame = passwordFrame;
open();
}
}

View File

@@ -19,22 +19,22 @@
package com.lyndir.masterpassword.gui;
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
import com.lyndir.masterpassword.model.MPConstants;
import com.lyndir.masterpassword.model.MPModelConstants;
/**
* @author lhunath, 2014-08-31
*/
@SuppressWarnings("CallToSystemGetenv")
public class Config {
public class MPConfig {
private static final Config instance = new Config();
private static final MPConfig instance = new MPConfig();
public static Config get() {
public static MPConfig get() {
return instance;
}
public boolean checkForUpdates() {
return ConversionUtils.toBoolean( System.getenv( MPConstants.env_checkUpdates ) ).orElse( true );
return ConversionUtils.toBoolean( System.getenv( MPModelConstants.env_checkUpdates ) ).orElse( true );
}
}

View File

@@ -0,0 +1,14 @@
package com.lyndir.masterpassword.gui;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import javax.swing.*;
/**
* @author lhunath, 2018-07-31
*/
public final class MPGuiConstants {
public static final KeyStroke ui_hotkey = KeyStroke.getKeyStroke( KeyEvent.VK_P, InputEvent.CTRL_DOWN_MASK | InputEvent.META_DOWN_MASK );
}

View File

@@ -0,0 +1,157 @@
//==============================================================================
// This file is part of Master Password.
// Copyright (c) 2011-2017, Maarten Billemont.
//
// Master Password is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Master Password is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You can find a copy of the GNU General Public License in the
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
//==============================================================================
package com.lyndir.masterpassword.gui;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.base.Charsets;
import com.google.common.io.ByteSource;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ObjectUtils;
import com.lyndir.masterpassword.gui.util.*;
import com.lyndir.masterpassword.gui.view.MasterPasswordFrame;
import com.lyndir.masterpassword.model.MPUser;
import com.tulskiy.keymaster.common.Provider;
import java.awt.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.*;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.Nullable;
import javax.swing.*;
/**
* <p> <i>Jun 10, 2008</i> </p>
*
* @author mbillemo
*/
public final class MasterPassword {
@SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MasterPassword.class );
private static final MasterPassword instance = new MasterPassword();
private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
@Nullable
private MasterPasswordFrame frame;
@Nullable
private MPUser<?> activeUser;
public static MasterPassword get() {
return instance;
}
public void addListener(final Listener listener) {
if (listeners.add( listener ))
listener.onUserSelected( activeUser );
}
public void removeListener(final Listener listener) {
listeners.remove( listener );
}
public void activateUser(final MPUser<?> user) {
if (ObjectUtils.equals( activeUser, user ))
return;
activeUser = user;
for (final Listener listener : listeners)
listener.onUserSelected( activeUser );
}
@Nullable
public String version() {
return MasterPassword.class.getPackage().getImplementationVersion();
}
public void open() {
Res.ui( () -> {
if (frame == null)
frame = new MasterPasswordFrame();
frame.setAlwaysOnTop( true );
frame.setVisible( true );
frame.setExtendedState( Frame.NORMAL );
Platform.get().requestForeground();
frame.setAlwaysOnTop( false );
} );
}
public void checkUpdate() {
try {
String implementationVersion = version();
String latestVersion = new ByteSource() {
@Override
public InputStream openStream()
throws IOException {
URL url = URI.create( "https://masterpassword.app/masterpassword-gui.jar.rev" ).toURL();
URLConnection conn = url.openConnection();
conn.addRequestProperty( "User-Agent", "masterpassword-gui" );
return conn.getInputStream();
}
}.asCharSource( Charsets.UTF_8 ).readFirstLine();
if ((implementationVersion != null) && !implementationVersion.equalsIgnoreCase( latestVersion )) {
logger.inf( "Implementation: <%s>", implementationVersion );
logger.inf( "Latest : <%s>", latestVersion );
logger.wrn( "You are not running the current official version. Please update from:%n%s",
"https://masterpassword.app/masterpassword-gui.jar" );
JOptionPane.showMessageDialog( null, Components.linkLabel( strf(
"A new version of Master Password is available."
+ "<p>Please download the latest version from <a href='https://masterpassword.app'>https://masterpassword.app</a>." ) ),
"Update Available", JOptionPane.INFORMATION_MESSAGE );
}
}
catch (final IOException e) {
logger.wrn( e, "Couldn't check for version update." );
}
}
public static void main(final String... args) {
//Thread.setDefaultUncaughtExceptionHandler(
// (t, e) -> logger.bug( e, "Uncaught: %s", e.getLocalizedMessage() ) );
// Try and set the system look & feel, if available.
try {
UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
Platform.get().installAppForegroundHandler( get()::open );
Platform.get().installAppReopenHandler( get()::open );
Provider.getCurrentProvider( true ).register( MPGuiConstants.ui_hotkey, hotKey -> get().open() );
}
catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
}
// Create a platform-specific GUI and open it.
get().open();
// Check online to see if this version has been superseded.
if (MPConfig.get().checkForUpdates())
get().checkUpdate();
}
@SuppressWarnings("InterfaceMayBeAnnotatedFunctional")
public interface Listener {
void onUserSelected(@Nullable MPUser<?> user);
}
}

View File

@@ -1,356 +0,0 @@
//==============================================================================
// This file is part of Master Password.
// Copyright (c) 2011-2017, Maarten Billemont.
//
// Master Password is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Master Password is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You can find a copy of the GNU General Public License in the
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
//==============================================================================
package com.lyndir.masterpassword.gui;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.collect.Maps;
import com.google.common.io.Resources;
import com.google.common.util.concurrent.JdkFutureAdapters;
import com.google.common.util.concurrent.ListenableFuture;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.MPIdenticon;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.ImageObserver;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.net.URL;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.*;
import org.jetbrains.annotations.NonNls;
/**
* @author lhunath, 2014-06-11
*/
@SuppressWarnings({ "HardcodedFileSeparator", "MethodReturnAlwaysConstant", "SpellCheckingInspection" })
public abstract class Res {
private static final int AVATAR_COUNT = 19;
private static final Map<Window, ScheduledExecutorService> jobExecutorByWindow = new WeakHashMap<>();
private static final Executor immediateUiExecutor = new SwingExecutorService( true );
private static final Executor laterUiExecutor = new SwingExecutorService( false );
private static final Logger logger = Logger.get( Res.class );
private static final Colors colors = new Colors();
public static Future<?> job(final Window host, final Runnable job) {
return job( host, job, 0, TimeUnit.MILLISECONDS );
}
public static Future<?> job(final Window host, final Runnable job, final long delay, final TimeUnit timeUnit) {
return jobExecutor( host ).schedule( () -> {
try {
job.run();
}
catch (final Throwable t) {
logger.err( t, "Unexpected: %s", t.getLocalizedMessage() );
}
}, delay, timeUnit );
}
public static <V> ListenableFuture<V> job(final Window host, final Callable<V> job) {
return job( host, job, 0, TimeUnit.MILLISECONDS );
}
public static <V> ListenableFuture<V> job(final Window host, final Callable<V> job, final long delay, final TimeUnit timeUnit) {
ScheduledExecutorService executor = jobExecutor( host );
return JdkFutureAdapters.listenInPoolThread( executor.schedule( job::call, delay, timeUnit ), executor );
}
public static Executor uiExecutor(final boolean immediate) {
return immediate? immediateUiExecutor: laterUiExecutor;
}
public static ScheduledExecutorService jobExecutor(final Window host) {
ScheduledExecutorService executor = jobExecutorByWindow.get( host );
if (executor == null) {
jobExecutorByWindow.put( host, executor = Executors.newSingleThreadScheduledExecutor() );
host.addWindowListener( new WindowAdapter() {
@Override
public void windowClosed(final WindowEvent e) {
ExecutorService executor = jobExecutorByWindow.remove( host );
if (executor != null)
executor.shutdownNow();
}
} );
}
return executor;
}
public static Icon iconAdd() {
return new RetinaIcon( Resources.getResource( "media/icon_add@2x.png" ) );
}
public static Icon iconDelete() {
return new RetinaIcon( Resources.getResource( "media/icon_delete@2x.png" ) );
}
public static Icon iconQuestion() {
return new RetinaIcon( Resources.getResource( "media/icon_question@2x.png" ) );
}
public static Icon avatar(final int index) {
return new RetinaIcon( Resources.getResource( strf( "media/avatar-%d@2x.png", index % avatars() ) ) );
}
public static int avatars() {
return AVATAR_COUNT;
}
public static Font emoticonsFont() {
return emoticonsRegular();
}
public static Font controlFont() {
return arimoRegular();
}
public static Font valueFont() {
return sourceSansProRegular();
}
public static Font bigValueFont() {
return sourceSansProBlack();
}
public static Font emoticonsRegular() {
return font( "fonts/Emoticons-Regular.otf" );
}
public static Font sourceCodeProRegular() {
return font( "fonts/SourceCodePro-Regular.otf" );
}
public static Font sourceCodeProBlack() {
return font( "fonts/SourceCodePro-Bold.otf" );
}
public static Font sourceSansProRegular() {
return font( "fonts/SourceSansPro-Regular.otf" );
}
public static Font sourceSansProBlack() {
return font( "fonts/SourceSansPro-Bold.otf" );
}
public static Font exoBold() {
return font( "fonts/Exo2.0-Bold.otf" );
}
public static Font exoExtraBold() {
return font( "fonts/Exo2.0-ExtraBold.otf" );
}
public static Font exoRegular() {
return font( "fonts/Exo2.0-Regular.otf" );
}
public static Font exoThin() {
return font( "fonts/Exo2.0-Thin.otf" );
}
public static Font arimoBold() {
return font( "fonts/Arimo-Bold.ttf" );
}
public static Font arimoBoldItalic() {
return font( "fonts/Arimo-BoldItalic.ttf" );
}
public static Font arimoItalic() {
return font( "fonts/Arimo-Italic.ttf" );
}
public static Font arimoRegular() {
return font( "fonts/Arimo-Regular.ttf" );
}
private static Font font(@NonNls final String fontResourceName) {
Map<String, SoftReference<Font>> fontsByResourceName = Maps.newHashMap();
SoftReference<Font> fontRef = fontsByResourceName.get( fontResourceName );
Font font = (fontRef == null)? null: fontRef.get();
if (font == null)
try {
fontsByResourceName.put( fontResourceName, new SoftReference<>(
font = Font.createFont( Font.TRUETYPE_FONT, Resources.getResource( fontResourceName ).openStream() ) ) );
}
catch (final FontFormatException | IOException e) {
throw logger.bug( e );
}
return font;
}
public static Colors colors() {
return colors;
}
private static final class RetinaIcon extends ImageIcon {
private static final Pattern scalePattern = Pattern.compile( ".*@(\\d+)x.[^.]+$" );
private static final long serialVersionUID = 1L;
private final float scale;
private RetinaIcon(final URL url) {
super( url );
Matcher scaleMatcher = scalePattern.matcher( url.getPath() );
scale = scaleMatcher.matches()? Float.parseFloat( scaleMatcher.group( 1 ) ): 1;
}
//private static URL retinaURL(final URL url) {
// try {
// final boolean[] isRetina = new boolean[1];
// new apple.awt.CImage.HiDPIScaledImage(1,1, BufferedImage.TYPE_INT_ARGB) {
// @Override
// public void drawIntoImage(BufferedImage image, float v) {
// isRetina[0] = v > 1;
// }
// };
// return isRetina[0];
// } catch (Throwable e) {
// e.printStackTrace();
// return url;
// }
//}
@Override
public int getIconWidth() {
return (int) (super.getIconWidth() / scale);
}
@Override
public int getIconHeight() {
return (int) (super.getIconHeight() / scale);
}
@Override
public synchronized void paintIcon(final Component c, final Graphics g, final int x, final int y) {
ImageObserver observer = ifNotNullElse( getImageObserver(), c );
Image image = getImage();
int width = image.getWidth( observer );
int height = image.getHeight( observer );
Graphics2D g2d = (Graphics2D) g.create( x, y, width, height );
g2d.scale( 1 / scale, 1 / scale );
g2d.drawImage( image, 0, 0, observer );
g2d.scale( 1, 1 );
g2d.dispose();
}
}
public static class Colors {
private final Color frameBg = Color.decode( "#5A5D6B" );
private final Color controlBg = Color.decode( "#ECECEC" );
private final Color controlBorder = Color.decode( "#BFBFBF" );
public Color frameBg() {
return frameBg;
}
public Color controlBg() {
return controlBg;
}
public Color controlBorder() {
return controlBorder;
}
public Color fromIdenticonColor(final MPIdenticon.Color identiconColor, final BackgroundMode backgroundMode) {
switch (identiconColor) {
case RED:
switch (backgroundMode) {
case DARK:
return Color.decode( "#dc322f" );
case LIGHT:
return Color.decode( "#dc322f" );
}
break;
case GREEN:
switch (backgroundMode) {
case DARK:
return Color.decode( "#859900" );
case LIGHT:
return Color.decode( "#859900" );
}
break;
case YELLOW:
switch (backgroundMode) {
case DARK:
return Color.decode( "#b58900" );
case LIGHT:
return Color.decode( "#b58900" );
}
break;
case BLUE:
switch (backgroundMode) {
case DARK:
return Color.decode( "#268bd2" );
case LIGHT:
return Color.decode( "#268bd2" );
}
break;
case MAGENTA:
switch (backgroundMode) {
case DARK:
return Color.decode( "#d33682" );
case LIGHT:
return Color.decode( "#d33682" );
}
break;
case CYAN:
switch (backgroundMode) {
case DARK:
return Color.decode( "#2aa198" );
case LIGHT:
return Color.decode( "#2aa198" );
}
break;
case MONO:
switch (backgroundMode) {
case DARK:
return Color.decode( "#93a1a1" );
case LIGHT:
return Color.decode( "#586e75" );
}
break;
}
throw new IllegalArgumentException( strf( "Color: %s or mode: %s not supported: ", identiconColor, backgroundMode ) );
}
public enum BackgroundMode {
DARK, LIGHT
}
}
}

View File

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

View File

@@ -29,25 +29,21 @@ import javax.annotation.Nullable;
/**
* @author lhunath, 14-12-16
*/
public class MPIncognitoSite extends MPBasicSite<MPIncognitoQuestion> {
public class MPIncognitoSite extends MPBasicSite<MPIncognitoUser, MPIncognitoQuestion> {
private final MPIncognitoUser user;
public MPIncognitoSite(final MPIncognitoUser user, final String name) {
this( user, name, null, null, null, null );
public MPIncognitoSite(final MPIncognitoUser user, final String siteName) {
this( user, siteName, null, null, null, null );
}
public MPIncognitoSite(final MPIncognitoUser user, final String name,
public MPIncognitoSite(final MPIncognitoUser user, final String siteName,
@Nullable final MPAlgorithm algorithm, @Nullable final UnsignedInteger counter,
@Nullable final MPResultType resultType, @Nullable final MPResultType loginType) {
super( name, (algorithm == null)? user.getAlgorithm(): algorithm, counter, resultType, loginType );
this.user = user;
super( user, siteName, (algorithm == null)? user.getAlgorithm(): algorithm, counter, resultType, loginType );
}
@Nonnull
@Override
public MPIncognitoUser getUser() {
return user;
public MPIncognitoQuestion addQuestion(final String keyword) {
return addQuestion( new MPIncognitoQuestion( this, keyword, null ) );
}
}

View File

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

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

@@ -0,0 +1,22 @@
package com.lyndir.masterpassword.gui.model;
import com.lyndir.masterpassword.model.*;
import com.lyndir.masterpassword.model.impl.*;
import javax.annotation.Nonnull;
/**
* @author lhunath, 2018-07-27
*/
public class MPNewSite extends MPBasicSite<MPUser<?>, MPQuestion> {
public MPNewSite(final MPUser<?> user, final String siteName) {
super( user, siteName );
}
@Nonnull
@Override
public MPQuestion addQuestion(final String keyword) {
throw new UnsupportedOperationException( "Cannot add a question to a site that hasn't been created yet." );
}
}

View File

@@ -1,57 +0,0 @@
//==============================================================================
// This file is part of Master Password.
// Copyright (c) 2011-2017, Maarten Billemont.
//
// Master Password is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Master Password is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You can find a copy of the GNU General Public License in the
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
//==============================================================================
package com.lyndir.masterpassword.gui.platform.mac;
import com.apple.eawt.*;
import com.google.common.base.Preconditions;
import com.lyndir.masterpassword.gui.GUI;
/**
* @author lhunath, 2014-06-10
*/
public class AppleGUI extends GUI {
static Application application;
static {
application = Preconditions.checkNotNull( Application.getApplication(), "Not an Apple Java application." );
}
public AppleGUI() {
application.addAppEventListener( new ApplicationListener() );
}
private class ApplicationListener implements AppForegroundListener, AppReOpenedListener {
@Override
public void appMovedToBackground(final AppEvent.AppForegroundEvent arg0) {
}
@Override
public void appRaisedToForeground(final AppEvent.AppForegroundEvent arg0) {
open();
}
@Override
public void appReOpened(final AppEvent.AppReOpenedEvent arg0) {
open();
}
}
}

View File

@@ -0,0 +1,157 @@
package com.lyndir.masterpassword.gui.util;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ObjectUtils;
import java.util.*;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
/**
* @author lhunath, 2018-07-19
*/
@SuppressWarnings("serial")
public class CollectionListModel<E> extends AbstractListModel<E>
implements ComboBoxModel<E>, ListSelectionListener, Selectable<E, CollectionListModel<E>> {
private static final Logger logger = Logger.get( CollectionListModel.class );
private final List<E> model = new LinkedList<>();
@Nullable
private JList<E> list;
@Nullable
private E selectedItem;
@Nullable
private Consumer<E> selectionConsumer;
@SafeVarargs
public CollectionListModel(final E... elements) {
this( Arrays.asList( elements ) );
}
public CollectionListModel(final Collection<? extends E> elements) {
model.addAll( elements );
selectedItem = getElementAt( 0 );
fireIntervalAdded( this, 0, model.size() );
}
@Override
public synchronized int getSize() {
return model.size();
}
@Nullable
@Override
public synchronized E getElementAt(final int index) {
return (index < model.size())? model.get( index ): null;
}
/**
* Replace this model's contents with the objects from the new model collection.
*
* This operation will mutate the internal model to reflect the given model.
* The given model will remain untouched and independent from this object.
*/
@SuppressWarnings({ "Guava", "AssignmentToForLoopParameter" })
public synchronized void set(final Iterable<? extends E> elements) {
ListIterator<E> oldIt = model.listIterator();
for (int from = 0; oldIt.hasNext(); ++from) {
int to = Iterables.indexOf( elements, Predicates.equalTo( oldIt.next() ) );
if (to != from) {
oldIt.remove();
fireIntervalRemoved( this, from, from );
--from;
}
}
int to = 0;
for (final E newSite : elements) {
if ((to >= model.size()) || !Objects.equals( model.get( to ), newSite )) {
model.add( to, newSite );
fireIntervalAdded( this, to, to );
}
++to;
}
if ((selectedItem == null) || !model.contains( selectedItem ))
setSelectedItem( getElementAt( 0 ) );
}
@SafeVarargs
public final synchronized void set(final E... elements) {
set( ImmutableList.copyOf( elements ) );
}
@Override
@SuppressWarnings({ "unchecked", "SuspiciousMethodCalls" })
public synchronized void setSelectedItem(@Nullable final Object newSelectedItem) {
if (!Objects.equals( selectedItem, newSelectedItem )) {
selectedItem = (E) newSelectedItem;
fireContentsChanged( this, -1, -1 );
//noinspection ObjectEquality
if ((list != null) && (list.getModel() == this))
list.setSelectedValue( selectedItem, true );
if (selectionConsumer != null)
selectionConsumer.accept( selectedItem );
}
}
@Nullable
@Override
public synchronized E getSelectedItem() {
return selectedItem;
}
public CollectionListModel<E> select(final E selectedItem) {
setSelectedItem( selectedItem );
return this;
}
public synchronized void registerList(final JList<E> list) {
// TODO: This class should probably implement ListSelectionModel instead.
if (this.list != null)
this.list.removeListSelectionListener( this );
this.list = list;
this.list.addListSelectionListener( this );
this.list.setModel( this );
}
@Override
public synchronized CollectionListModel<E> selection(@Nullable final Consumer<E> selectionConsumer) {
this.selectionConsumer = selectionConsumer;
if (selectionConsumer != null)
selectionConsumer.accept( selectedItem );
return this;
}
@Override
public synchronized CollectionListModel<E> selection(@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
this.selectionConsumer = null;
setSelectedItem( selectedItem );
return selection( selectionConsumer );
}
@Override
public synchronized void valueChanged(final ListSelectionEvent event) {
//noinspection ObjectEquality
if (!event.getValueIsAdjusting() && (event.getSource() == list) && (list.getModel() == this)) {
selectedItem = list.getSelectedValue();
if (selectionConsumer != null)
selectionConsumer.accept( selectedItem );
}
}
}

View File

@@ -18,24 +18,51 @@
package com.lyndir.masterpassword.gui.util;
import com.lyndir.masterpassword.gui.Res;
import com.google.common.base.Strings;
import com.lyndir.lhunath.opal.system.logging.Logger;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collection;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.annotation.Nullable;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.event.HyperlinkEvent;
import javax.swing.text.*;
import org.jetbrains.annotations.NonNls;
/**
* @author lhunath, 2014-06-08
*/
@SuppressWarnings({ "SerializableStoresNonSerializable", "serial" })
public abstract class Components {
private static final float CONTROL_TEXT_SIZE = 12f;
private static final Logger logger = Logger.get( Components.class );
public static GradientPanel boxLayout(final int axis, final Component... components) {
GradientPanel container = gradientPanel( null, null );
// container.setBackground( Color.red );
public static final float TEXT_SIZE_HEADING = 19f;
public static final float TEXT_SIZE_CONTROL = 13f;
public static final int SIZE_MARGIN = 12;
public static final int SIZE_PADDING = 8;
public static GradientPanel panel(final Component... components) {
GradientPanel panel = panel( BoxLayout.LINE_AXIS, null, components );
panel.setLayout( new OverlayLayout( panel ) );
return panel;
}
public static GradientPanel panel(final int axis, final Component... components) {
return panel( axis, null, components );
}
public static GradientPanel panel(final int axis, @Nullable final Color background, final Component... components) {
GradientPanel container = panel( null, background );
container.setLayout( new BoxLayout( container, axis ) );
for (final Component component : components)
container.add( component );
@@ -43,46 +70,142 @@ public abstract class Components {
return container;
}
public static GradientPanel borderPanel(final JComponent component, @Nullable final Border border) {
return borderPanel( component, border, null );
public static GradientPanel borderPanel(final int axis, final Component... components) {
return borderPanel( marginBorder(), null, axis, components );
}
public static GradientPanel borderPanel(final JComponent component, @Nullable final Border border, @Nullable final Color background) {
GradientPanel box = boxLayout( BoxLayout.LINE_AXIS, component );
public static GradientPanel borderPanel(@Nullable final Border border, final int axis, final Component... components) {
return borderPanel( border, null, axis, components );
}
public static GradientPanel borderPanel(@Nullable final Color background, final int axis, final Component... components) {
return borderPanel( marginBorder(), background, axis, components );
}
public static GradientPanel borderPanel(@Nullable final Border border, @Nullable final Color background, final int axis,
final Component... components) {
GradientPanel box = panel( axis, background, components );
if (border != null)
box.setBorder( border );
if (background != null)
box.setBackground( background );
return box;
}
public static GradientPanel gradientPanel(@Nullable final LayoutManager layout, @Nullable final Color color) {
return new GradientPanel( layout, color ) {
{
setOpaque( color != null );
setBackground( null );
setAlignmentX( LEFT_ALIGNMENT );
setAlignmentY( BOTTOM_ALIGNMENT );
}
};
public static GradientPanel panel(@Nullable final LayoutManager layout) {
return panel( layout, null );
}
public static GradientPanel panel(@Nullable final LayoutManager layout, @Nullable final Color color) {
return new GradientPanel( layout, color );
}
public static int showDialog(@Nullable final Component owner, @Nullable final String title, final JOptionPane pane) {
JDialog dialog = pane.createDialog( owner, title );
dialog.setMinimumSize( new Dimension( 520, 0 ) );
dialog.setModalityType( Dialog.ModalityType.DOCUMENT_MODAL );
showDialog( dialog );
Object selectedValue = pane.getValue();
if (selectedValue == null)
return JOptionPane.CLOSED_OPTION;
Object[] options = pane.getOptions();
if (options == null)
return (selectedValue instanceof Integer)? (Integer) selectedValue: JOptionPane.CLOSED_OPTION;
try {
int option = Arrays.binarySearch( options, selectedValue );
return (option < 0)? JOptionPane.CLOSED_OPTION: option;
}
catch (final ClassCastException ignored) {
return JOptionPane.CLOSED_OPTION;
}
}
@Nullable
public static File showLoadDialog(@Nullable final Component owner, final String title) {
return showFileDialog( owner, title, FileDialog.LOAD, null );
}
@Nullable
public static File showSaveDialog(@Nullable final Component owner, final String title, final String fileName) {
return showFileDialog( owner, title, FileDialog.SAVE, fileName );
}
@Nullable
private static File showFileDialog(@Nullable final Component owner, final String title,
final int mode, @Nullable final String fileName) {
FileDialog fileDialog = new FileDialog( JOptionPane.getFrameForComponent( owner ), title, mode );
fileDialog.setFile( fileName );
fileDialog.setLocationRelativeTo( owner );
fileDialog.setLocationByPlatform( true );
fileDialog.setVisible( true );
File[] selectedFiles = fileDialog.getFiles();
return ((selectedFiles != null) && (selectedFiles.length > 0))? selectedFiles[0]: null;
}
public static JDialog showDialog(@Nullable final Component owner, @Nullable final String title, final Container content) {
JDialog dialog = new JDialog( (owner != null)? SwingUtilities.windowForComponent( owner ): null,
title, Dialog.ModalityType.DOCUMENT_MODAL );
dialog.setMinimumSize( new Dimension( 320, 0 ) );
dialog.setLocationRelativeTo( owner );
dialog.setContentPane( content );
return showDialog( dialog );
}
private static JDialog showDialog(final JDialog dialog) {
// OpenJDK does not correctly implement this setting in native code.
dialog.getRootPane().putClientProperty( "apple.awt.documentModalSheet", Boolean.TRUE );
dialog.getRootPane().putClientProperty( "Window.style", "small" );
dialog.pack();
dialog.setLocationByPlatform( true );
dialog.setVisible( true );
return dialog;
}
public static JTextField textField() {
return new JTextField() {
return textField( null );
}
public static JTextField textField(@Nullable final Document document) {
return new JTextField( document, null, 0 ) {
{
setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) );
setFont( Res.valueFont().deriveFont( CONTROL_TEXT_SIZE ) );
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
setAlignmentX( LEFT_ALIGNMENT );
setAlignmentY( BOTTOM_ALIGNMENT );
}
@Override
public Dimension getMaximumSize() {
return new Dimension( Integer.MAX_VALUE, getPreferredSize().height );
return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE );
}
};
}
public static JTextField textField(@Nullable final String text, @Nullable final Consumer<String> change) {
return textField( new DocumentModel( new PlainDocument() ).selection( text, change ).getDocument() );
}
public static JTextArea textArea() {
return new JTextArea() {
{
setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) );
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
setAlignmentX( LEFT_ALIGNMENT );
setLineWrap( true );
setRows( 3 );
}
@Override
public Dimension getMaximumSize() {
return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE );
}
};
}
@@ -93,7 +216,6 @@ public abstract class Components {
setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) );
setAlignmentX( LEFT_ALIGNMENT );
setAlignmentY( BOTTOM_ALIGNMENT );
}
@Override
@@ -103,49 +225,177 @@ public abstract class Components {
};
}
public static JButton button(final String label) {
return new JButton( label ) {
public static <E> JList<E> list(final ListModel<E> model, final Function<E, String> valueTransformer) {
return new JList<E>( model ) {
{
setFont( Res.controlFont().deriveFont( CONTROL_TEXT_SIZE ) );
setAlignmentX( LEFT_ALIGNMENT );
setAlignmentY( BOTTOM_ALIGNMENT );
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
setCellRenderer( new DefaultListCellRenderer() {
@Override
@SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" })
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
final boolean isSelected, final boolean cellHasFocus) {
String label = valueTransformer.apply( (E) value );
super.getListCellRendererComponent(
list, Strings.isNullOrEmpty( label )? " ": label, index, isSelected, cellHasFocus );
setBorder( BorderFactory.createEmptyBorder( 2, 4, 2, 4 ) );
return this;
}
} );
if (model instanceof CollectionListModel)
((CollectionListModel<E>) model).registerList( this );
}
@Override
public Dimension getMaximumSize() {
return new Dimension( 20, getPreferredSize().height );
return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE );
}
};
}
public static Component stud() {
Dimension studDimension = new Dimension( 8, 8 );
public static JScrollPane scrollPane(final Component child) {
return new JScrollPane( child ) {
{
setBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ) );
setAlignmentX( LEFT_ALIGNMENT );
}
};
}
public static JButton button(final String label, @Nullable final ActionListener actionListener) {
return button( new AbstractAction( label ) {
@Override
public void actionPerformed(final ActionEvent e) {
if (actionListener != null)
actionListener.actionPerformed( e );
}
@Override
public boolean isEnabled() {
return actionListener != null;
}
} );
}
public static JButton button(final Icon icon, @Nullable final ActionListener actionListener, @Nullable String toolTip) {
JButton iconButton = button( new AbstractAction( null, icon ) {
@Override
public void actionPerformed(final ActionEvent e) {
if (actionListener != null)
actionListener.actionPerformed( e );
}
@Override
public boolean isEnabled() {
return actionListener != null;
}
} );
iconButton.setToolTipText( toolTip );
iconButton.setFocusable( false );
return iconButton;
}
public static JButton button(final Action action) {
return new JButton( action ) {
{
setAlignmentX( LEFT_ALIGNMENT );
if (getText() == null) {
setContentAreaFilled( false );
setBorderPainted( false );
setOpaque( false );
}
}
};
}
public static Component strut() {
return strut( SIZE_PADDING );
}
public static Component strut(final int size) {
Dimension studDimension = new Dimension( size, size );
Box.Filler rigidArea = new Box.Filler( studDimension, studDimension, studDimension );
rigidArea.setAlignmentX( Component.LEFT_ALIGNMENT );
rigidArea.setAlignmentY( Component.BOTTOM_ALIGNMENT );
rigidArea.setBackground( Color.red );
return rigidArea;
}
public static int margin() {
return SIZE_MARGIN;
}
public static Border marginBorder() {
return marginBorder( margin() );
}
public static Border marginBorder(final int size) {
return BorderFactory.createEmptyBorder( size, size, size, size );
}
public static JSpinner spinner(final SpinnerModel model) {
return new JSpinner( model ) {
{
CompoundBorder editorBorder = BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) );
DefaultFormatterFactory formatterFactory = new DefaultFormatterFactory();
if (model instanceof UnsignedIntegerModel)
formatterFactory.setDefaultFormatter( ((UnsignedIntegerModel) model).getFormatter() );
((DefaultEditor) getEditor()).getTextField().setFormatterFactory( formatterFactory );
((DefaultEditor) getEditor()).getTextField().setBorder( editorBorder );
setAlignmentX( LEFT_ALIGNMENT );
setAlignmentY( BOTTOM_ALIGNMENT );
setBorder( null );
}
};
}
public static JLabel heading() {
return heading( " " );
}
public static JLabel heading(final int horizontalAlignment) {
return heading( " ", horizontalAlignment );
}
public static JLabel heading(@Nullable final String heading) {
return heading( heading, SwingConstants.CENTER );
}
/**
* @param horizontalAlignment One of the following constants
* defined in {@code SwingConstants}:
* {@code LEFT},
* {@code CENTER},
* {@code RIGHT},
* {@code LEADING} or
* {@code TRAILING}.
*/
public static JLabel heading(@Nullable final String heading, final int horizontalAlignment) {
return new JLabel( heading, horizontalAlignment ) {
{
setFont( getFont().deriveFont( Font.BOLD, TEXT_SIZE_HEADING ) );
setAlignmentX( LEFT_ALIGNMENT );
}
@Override
public Dimension getMaximumSize() {
return new Dimension( 20, getPreferredSize().height );
return new Dimension( Integer.MAX_VALUE, getPreferredSize().height );
}
};
}
public static JLabel label() {
return label( " " );
}
public static JLabel label(final int horizontalAlignment) {
return label( " ", horizontalAlignment );
}
public static JLabel label(@Nullable final String label) {
return label( label, SwingConstants.LEADING );
}
@@ -162,9 +412,7 @@ public abstract class Components {
public static JLabel label(@Nullable final String label, final int horizontalAlignment) {
return new JLabel( label, horizontalAlignment ) {
{
setFont( Res.controlFont().deriveFont( CONTROL_TEXT_SIZE ) );
setAlignmentX( LEFT_ALIGNMENT );
setAlignmentY( BOTTOM_ALIGNMENT );
}
@Override
@@ -177,30 +425,52 @@ public abstract class Components {
public static JCheckBox checkBox(final String label) {
return new JCheckBox( label ) {
{
setFont( Res.controlFont().deriveFont( CONTROL_TEXT_SIZE ) );
setBackground( null );
setAlignmentX( LEFT_ALIGNMENT );
setAlignmentY( BOTTOM_ALIGNMENT );
}
};
}
@SafeVarargs
public static <V> JComboBox<V> comboBox(final V... values) {
return comboBox( new DefaultComboBoxModel<>( values ) );
public static <E> JComboBox<E> comboBox(final Function<E, String> valueTransformer, final E... values) {
return comboBox( new DefaultComboBoxModel<>( values ), valueTransformer );
}
public static <M> JComboBox<M> comboBox(final ComboBoxModel<M> model) {
return new JComboBox<M>( model ) {
public static <E> JComboBox<E> comboBox(final E[] values, final Function<E, String> valueTransformer,
@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
return comboBox( new CollectionListModel<>( values ).selection( selectedItem, selectionConsumer ), valueTransformer );
}
public static <E> JComboBox<E> comboBox(final Collection<E> values, final Function<E, String> valueTransformer,
@Nullable final Consumer<E> selectionConsumer) {
return comboBox( new CollectionListModel<>( values ).selection( selectionConsumer ), valueTransformer );
}
public static <E> JComboBox<E> comboBox(final Collection<E> values, final Function<E, String> valueTransformer,
@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
return comboBox( new CollectionListModel<>( values ).selection( selectedItem, selectionConsumer ), valueTransformer );
}
public static <E> JComboBox<E> comboBox(final ComboBoxModel<E> model, final Function<E, String> valueTransformer) {
return new JComboBox<E>( model ) {
{
// CompoundBorder editorBorder = BorderFactory.createCompoundBorder(
// BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
// BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) );
// ((JComponent) ((BasicComboBoxEditor) getEditor()).getEditorComponent()).setBorder(editorBorder);
setFont( Res.controlFont().deriveFont( CONTROL_TEXT_SIZE ) );
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
setBorder( BorderFactory.createEmptyBorder( 4, 0, 4, 0 ) );
setRenderer( new DefaultListCellRenderer() {
@Override
@SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" })
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
final boolean isSelected, final boolean cellHasFocus) {
String label = valueTransformer.apply( (E) value );
super.getListCellRendererComponent(
list, Strings.isNullOrEmpty( label )? " ": label, index, isSelected, cellHasFocus );
setBorder( BorderFactory.createEmptyBorder( 0, 4, 0, 4 ) );
return this;
}
} );
putClientProperty( "JComboBox.isPopDown", Boolean.TRUE );
setAlignmentX( LEFT_ALIGNMENT );
setAlignmentY( BOTTOM_ALIGNMENT );
// setBorder(null);
}
@Override
@@ -210,6 +480,24 @@ public abstract class Components {
};
}
public static JEditorPane linkLabel(@NonNls final String html) {
return new JEditorPane( "text/html", "<html><body style='width:640;font-family:sans-serif'>" + html ) {
{
setOpaque( false );
setEditable( false );
addHyperlinkListener( event -> {
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED)
try {
Platform.get().open( event.getURL().toURI() );
}
catch (final URISyntaxException e) {
logger.err( e, "After triggering hyperlink: %s", event );
}
} );
}
};
}
public static class GradientPanel extends JPanel {
@Nullable
@@ -218,10 +506,26 @@ public abstract class Components {
@Nullable
private GradientPaint paint;
protected GradientPanel(@Nullable final LayoutManager layout, @Nullable final Color gradientColor) {
public GradientPanel() {
this( null, null );
}
public GradientPanel(@Nullable final Color gradientColor) {
this( null, gradientColor );
}
public GradientPanel(@Nullable final LayoutManager layout) {
this( layout, null );
}
public GradientPanel(@Nullable final LayoutManager layout, @Nullable final Color gradientColor) {
super( layout );
this.gradientColor = gradientColor;
if (getLayout() == null)
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
setGradientColor( gradientColor );
setBackground( null );
setAlignmentX( LEFT_ALIGNMENT );
}
@Nullable
@@ -231,17 +535,30 @@ public abstract class Components {
public void setGradientColor(@Nullable final Color gradientColor) {
this.gradientColor = gradientColor;
revalidate();
updatePaint();
}
@Override
public void doLayout() {
super.doLayout();
public void setBackground(@Nullable final Color bg) {
super.setBackground( bg );
setOpaque( bg != null );
}
if (gradientColor != null) {
paint = new GradientPaint( new Point( 0, 0 ), gradientColor, new Point( getWidth(), getHeight() ), gradientColor.darker() );
repaint();
@Override
public void setBounds(final int x, final int y, final int width, final int height) {
super.setBounds( x, y, width, height );
updatePaint();
}
private void updatePaint() {
if (gradientColor == null) {
paint = null;
return;
}
paint = new GradientPaint( new Point( 0, 0 ), gradientColor,
new Point( getWidth(), getHeight() ), gradientColor.darker() );
repaint();
}
@Override

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

View File

@@ -0,0 +1,52 @@
package com.lyndir.masterpassword.gui.util;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.TypeUtils;
import com.lyndir.masterpassword.gui.util.platform.BasePlatform;
import com.lyndir.masterpassword.gui.util.platform.IPlatform;
import java.lang.reflect.InvocationTargetException;
import java.util.Optional;
import javax.annotation.Nullable;
/**
* @author lhunath, 2018-07-29
*/
public final class Platform {
private static final Logger logger = Logger.get( Platform.class );
private static final IPlatform activePlatform;
static {
IPlatform tryPlatform;
if (null != (tryPlatform = construct( "com.lyndir.masterpassword.gui.util.platform.JDK9Platform" )))
activePlatform = tryPlatform;
else if (null != (tryPlatform = construct( "com.lyndir.masterpassword.gui.util.platform.ApplePlatform" )))
activePlatform = tryPlatform;
else
activePlatform = new BasePlatform();
}
@Nullable
private static <T> T construct(final String typeName) {
try {
// AppleGUI adds support for macOS features.
Optional<Class<T>> gui = TypeUtils.loadClass( typeName );
if (gui.isPresent())
return gui.get().getConstructor().newInstance();
}
catch (@SuppressWarnings("ErrorNotRethrown") final LinkageError ignored) {
}
catch (final IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
throw logger.bug( e );
}
return null;
}
public static IPlatform get() {
return activePlatform;
}
}

View File

@@ -0,0 +1,373 @@
//==============================================================================
// This file is part of Master Password.
// Copyright (c) 2011-2017, Maarten Billemont.
//
// Master Password is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Master Password is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You can find a copy of the GNU General Public License in the
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
//==============================================================================
package com.lyndir.masterpassword.gui.util;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.collect.Maps;
import com.google.common.io.Resources;
import com.google.common.util.concurrent.*;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.MPIdenticon;
import java.awt.*;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.Map;
import java.util.concurrent.*;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import javax.swing.*;
import org.jetbrains.annotations.NonNls;
import org.joda.time.*;
import org.joda.time.format.DateTimeFormat;
/**
* @author lhunath, 2014-06-11
*/
@SuppressWarnings({ "HardcodedFileSeparator", "MethodReturnAlwaysConstant", "SpellCheckingInspection", "serial" })
public abstract class Res {
private static final int AVATAR_COUNT = 19;
private static final ListeningScheduledExecutorService jobExecutor = MoreExecutors.listeningDecorator(
Executors.newSingleThreadScheduledExecutor() );
private static final Executor immediateUiExecutor = new SwingExecutorService( true );
private static final Executor laterUiExecutor = new SwingExecutorService( false );
private static final Logger logger = Logger.get( Res.class );
private static final Icons icons = new Icons();
private static final Fonts fonts = new Fonts();
private static final Colors colors = new Colors();
public static Future<?> job(final Runnable job) {
return job( job, 0, TimeUnit.MILLISECONDS );
}
public static Future<?> job(final Runnable job, final long delay, final TimeUnit timeUnit) {
return jobExecutor.schedule( () -> {
try {
job.run();
}
catch (final Throwable t) {
logger.err( t, "Unexpected: %s", t.getLocalizedMessage() );
}
}, delay, timeUnit );
}
public static <V> ListenableFuture<V> job(final Callable<V> job) {
return job( job, 0, TimeUnit.MILLISECONDS );
}
public static <V> void job(final Callable<V> job, final Consumer<V> callback) {
Futures.addCallback( job( job, 0, TimeUnit.MILLISECONDS ), new FailableCallback<V>( logger ) {
@Override
public void onSuccess(@Nullable final V result) {
callback.accept( result );
}
}, uiExecutor() );
}
public static <V> ListenableFuture<V> job(final Callable<V> job, final long delay, final TimeUnit timeUnit) {
return jobExecutor.schedule( job, delay, timeUnit );
}
public static void ui(final Runnable job) {
ui( true, job );
}
public static void ui(final boolean immediate, final Runnable job) {
uiExecutor( immediate ).execute( job );
}
public static Executor uiExecutor() {
return uiExecutor( true );
}
public static Executor uiExecutor(final boolean immediate) {
return immediate? immediateUiExecutor: laterUiExecutor;
}
public static Icons icons() {
return icons;
}
public static Fonts fonts() {
return fonts;
}
public static Colors colors() {
return colors;
}
public static String format(final ReadableInstant instant) {
return DateTimeFormat.mediumDateTime().print( new DateTime( instant, DateTimeZone.getDefault() ) );
}
public static final class Icons {
public Icon add() {
return icon( "media/icon_add.png" );
}
public Icon delete() {
return icon( "media/icon_delete.png" );
}
public Icon question() {
return icon( "media/icon_question.png" );
}
public Icon user() {
return icon( "media/icon_user.png" );
}
public Icon lock() {
return icon( "media/icon_lock.png" );
}
public Icon reset() {
return icon( "media/icon_reset.png" );
}
public Icon import_() {
return icon( "media/icon_import.png" );
}
public Icon help() {
return icon( "media/icon_help.png" );
}
public Icon export() {
return icon( "media/icon_export.png" );
}
public Icon settings() {
return icon( "media/icon_settings.png" );
}
public Icon edit() {
return icon( "media/icon_edit.png" );
}
public Icon key() {
return icon( "media/icon_key.png" );
}
public Icon avatar(final int index) {
return icon( strf( "media/avatar-%d.png", index % avatars() ) );
}
public int avatars() {
return AVATAR_COUNT;
}
private static Icon icon(@NonNls final String resourceName) {
return new ImageIcon( Toolkit.getDefaultToolkit().getImage( Res.class.getClassLoader().getResource( resourceName ) ) );
}
}
public static final class Fonts {
public Font emoticonsFont(final float size) {
return emoticonsRegular().deriveFont( size );
}
public Font controlFont(final float size) {
return exoRegular().deriveFont( size );
}
public Font valueFont(final float size) {
return sourceSansProRegular().deriveFont( size );
}
public Font bigValueFont(final float size) {
return sourceSansProBlack().deriveFont( size );
}
public Font emoticonsRegular() {
return font( "fonts/Emoticons-Regular.otf" );
}
public Font sourceCodeProRegular() {
return font( "fonts/SourceCodePro-Regular.otf" );
}
public Font sourceCodeProBlack() {
return font( "fonts/SourceCodePro-Bold.otf" );
}
public Font sourceSansProRegular() {
return font( "fonts/SourceSansPro-Regular.otf" );
}
public Font sourceSansProBlack() {
return font( "fonts/SourceSansPro-Bold.otf" );
}
public Font exoBold() {
return font( "fonts/Exo2.0-Bold.otf" );
}
public Font exoExtraBold() {
return font( "fonts/Exo2.0-ExtraBold.otf" );
}
public Font exoRegular() {
return font( "fonts/Exo2.0-Regular.otf" );
}
public Font exoThin() {
return font( "fonts/Exo2.0-Thin.otf" );
}
public Font arimoBold() {
return font( "fonts/Arimo-Bold.ttf" );
}
public Font arimoBoldItalic() {
return font( "fonts/Arimo-BoldItalic.ttf" );
}
public Font arimoItalic() {
return font( "fonts/Arimo-Italic.ttf" );
}
public Font arimoRegular() {
return font( "fonts/Arimo-Regular.ttf" );
}
private static Font font(@NonNls final String fontResourceName) {
Map<String, SoftReference<Font>> fontsByResourceName = Maps.newHashMap();
SoftReference<Font> fontRef = fontsByResourceName.get( fontResourceName );
Font font = (fontRef == null)? null: fontRef.get();
if (font == null)
try {
fontsByResourceName.put( fontResourceName, new SoftReference<>(
font = Font.createFont( Font.TRUETYPE_FONT, Resources.getResource( fontResourceName ).openStream() ) ) );
}
catch (final FontFormatException | IOException e) {
throw logger.bug( e );
}
return font;
}
}
public static final class Colors {
private final Color transparent = new Color( 0, 0, 0, 0 );
private final Color frameBg = Color.decode( "#5A5D6B" );
private final Color controlBg = SystemColor.window;
private final Color controlBorder = Color.decode( "#BFBFBF" );
private final Color highlightFg = SystemColor.controlHighlight;
private final Color errorFg = Color.decode( "#FF3333" );
public Color transparent() {
return transparent;
}
public Color frameBg() {
return frameBg;
}
public Color controlBg() {
return controlBg;
}
public Color controlBorder() {
return controlBorder;
}
public Color highlightFg() {
return highlightFg;
}
public Color errorFg() {
return errorFg;
}
public Color fromIdenticonColor(final MPIdenticon.Color identiconColor, final BackgroundMode backgroundMode) {
switch (identiconColor) {
case RED:
switch (backgroundMode) {
case DARK:
return Color.decode( "#dc322f" );
case LIGHT:
return Color.decode( "#dc322f" );
}
break;
case GREEN:
switch (backgroundMode) {
case DARK:
return Color.decode( "#859900" );
case LIGHT:
return Color.decode( "#859900" );
}
break;
case YELLOW:
switch (backgroundMode) {
case DARK:
return Color.decode( "#b58900" );
case LIGHT:
return Color.decode( "#b58900" );
}
break;
case BLUE:
switch (backgroundMode) {
case DARK:
return Color.decode( "#268bd2" );
case LIGHT:
return Color.decode( "#268bd2" );
}
break;
case MAGENTA:
switch (backgroundMode) {
case DARK:
return Color.decode( "#d33682" );
case LIGHT:
return Color.decode( "#d33682" );
}
break;
case CYAN:
switch (backgroundMode) {
case DARK:
return Color.decode( "#2aa198" );
case LIGHT:
return Color.decode( "#2aa198" );
}
break;
case MONO:
switch (backgroundMode) {
case DARK:
return Color.decode( "#93a1a1" );
case LIGHT:
return Color.decode( "#586e75" );
}
break;
}
throw new IllegalArgumentException( strf( "Color: %s or mode: %s not supported: ", identiconColor, backgroundMode ) );
}
public enum BackgroundMode {
DARK, LIGHT
}
}
}

View File

@@ -0,0 +1,15 @@
package com.lyndir.masterpassword.gui.util;
import java.util.function.Consumer;
import javax.annotation.Nullable;
/**
* @author lhunath, 2018-07-26
*/
public interface Selectable<E, T> {
T selection(@Nullable Consumer<E> selectionConsumer);
T selection(@Nullable E selectedItem, @Nullable Consumer<E> selectionConsumer);
}

View File

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

View File

@@ -19,15 +19,21 @@
package com.lyndir.masterpassword.gui.util;
import com.google.common.primitives.UnsignedInteger;
import java.text.ParseException;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import javax.swing.*;
import javax.swing.event.ChangeListener;
/**
* @author lhunath, 2016-10-29
*/
public class UnsignedIntegerModel extends SpinnerNumberModel {
@SuppressWarnings("serial")
public class UnsignedIntegerModel extends SpinnerNumberModel implements Selectable<UnsignedInteger, UnsignedIntegerModel> {
private static final long serialVersionUID = 1L;
@Nullable
private ChangeListener changeListener;
public UnsignedIntegerModel() {
this( UnsignedInteger.ZERO, UnsignedInteger.ZERO, UnsignedInteger.MAX_VALUE, UnsignedInteger.ONE );
@@ -55,4 +61,80 @@ public class UnsignedIntegerModel extends SpinnerNumberModel {
public UnsignedInteger getNumber() {
return (UnsignedInteger) super.getNumber();
}
@Override
public UnsignedInteger getMinimum() {
return (UnsignedInteger) super.getMinimum();
}
@Override
public UnsignedInteger getMaximum() {
return (UnsignedInteger) super.getMaximum();
}
@Override
public UnsignedInteger getStepSize() {
return (UnsignedInteger) super.getStepSize();
}
@Override
public UnsignedInteger getNextValue() {
if ((getMaximum() == null) || (getMaximum().compareTo( getNumber() ) > 0))
return getNumber().plus( getStepSize() );
return getMaximum();
}
@Override
public UnsignedInteger getPreviousValue() {
if ((getMinimum() == null) || (getMinimum().compareTo( getNumber() ) < 0))
return getNumber().minus( getStepSize() );
return getMinimum();
}
@Override
public UnsignedIntegerModel selection(@Nullable final Consumer<UnsignedInteger> selectionConsumer) {
if (changeListener != null) {
removeChangeListener( changeListener );
changeListener = null;
}
if (selectionConsumer != null) {
addChangeListener( changeListener = e -> selectionConsumer.accept( getNumber() ) );
selectionConsumer.accept( getNumber() );
}
return this;
}
@Override
public UnsignedIntegerModel selection(@Nullable final UnsignedInteger selectedItem,
@Nullable final Consumer<UnsignedInteger> selectionConsumer) {
if (changeListener != null) {
removeChangeListener( changeListener );
changeListener = null;
}
setValue( (selectedItem != null)? selectedItem: getMinimum() );
return selection( selectionConsumer );
}
public JFormattedTextField.AbstractFormatter getFormatter() {
return new JFormattedTextField.AbstractFormatter() {
@Override
@Nullable
public Object stringToValue(@Nullable final String text)
throws ParseException {
return (text != null)? UnsignedInteger.valueOf( text ): null;
}
@Override
@Nullable
public String valueToString(final Object value)
throws ParseException {
return (value != null)? value.toString(): null;
}
};
}
}

View File

@@ -0,0 +1,69 @@
package com.lyndir.masterpassword.gui.util.platform;
import com.apple.eawt.*;
import com.apple.eio.FileManager;
import com.google.common.base.Preconditions;
import com.lyndir.lhunath.opal.system.logging.Logger;
import java.io.*;
import java.net.URI;
/**
* @author lhunath, 2018-07-29
*/
public class ApplePlatform implements IPlatform {
private static final Logger logger = Logger.get( ApplePlatform.class );
private static final Application application = Preconditions.checkNotNull(
Application.getApplication(), "Not an Apple Java application." );
@Override
public boolean installAppForegroundHandler(final Runnable handler) {
application.addAppEventListener( new AppForegroundListener() {
@Override
public void appMovedToBackground(final AppEvent.AppForegroundEvent e) {
}
@Override
public void appRaisedToForeground(final AppEvent.AppForegroundEvent e) {
handler.run();
}
} );
return true;
}
@Override
public boolean installAppReopenHandler(final Runnable handler) {
application.addAppEventListener( (AppReOpenedListener) e -> handler.run() );
return true;
}
@Override
public boolean requestForeground() {
application.requestForeground( true );
return true;
}
@Override
public boolean show(final File file) {
try {
return FileManager.revealInFinder( file );
}
catch (final FileNotFoundException e) {
logger.err( e, "While showing: %s", file );
return false;
}
}
@Override
public boolean open(final URI url) {
try {
FileManager.openURL( url.toString() );
return true;
}
catch (final IOException e) {
logger.err( e, "While opening: %s", url );
return false;
}
}
}

View File

@@ -0,0 +1,36 @@
package com.lyndir.masterpassword.gui.util.platform;
import java.io.File;
import java.net.URI;
/**
* @author lhunath, 2018-07-29
*/
public class BasePlatform implements IPlatform {
@Override
public boolean installAppForegroundHandler(final Runnable handler) {
return false;
}
@Override
public boolean installAppReopenHandler(final Runnable handler) {
return false;
}
@Override
public boolean requestForeground() {
return false;
}
@Override
public boolean show(final File file) {
return false;
}
@Override
public boolean open(final URI url) {
return false;
}
}

View File

@@ -0,0 +1,22 @@
package com.lyndir.masterpassword.gui.util.platform;
import java.io.File;
import java.net.URI;
import java.net.URL;
/**
* @author lhunath, 2018-07-29
*/
public interface IPlatform {
boolean installAppForegroundHandler(Runnable handler);
boolean installAppReopenHandler(Runnable handler);
boolean requestForeground();
boolean show(File file);
boolean open(URI url);
}

View File

@@ -0,0 +1,67 @@
package com.lyndir.masterpassword.gui.util.platform;
import com.lyndir.lhunath.opal.system.logging.Logger;
import java.awt.*;
import java.awt.desktop.*;
import java.io.File;
import java.io.IOException;
import java.net.URI;
/**
* @author lhunath, 2018-07-29
*/
@SuppressWarnings("Since15")
public class JDK9Platform implements IPlatform {
private static final Logger logger = Logger.get( JDK9Platform.class );
private static final Desktop desktop = Desktop.getDesktop();
@Override
public boolean installAppForegroundHandler(final Runnable handler) {
desktop.addAppEventListener( new AppForegroundListener() {
@Override
public void appRaisedToForeground(final AppForegroundEvent e) {
handler.run();
}
@Override
public void appMovedToBackground(final AppForegroundEvent e) {
}
} );
return true;
}
@Override
public boolean installAppReopenHandler(final Runnable handler) {
desktop.addAppEventListener( (AppReopenedListener) e -> handler.run() );
return true;
}
@Override
public boolean requestForeground() {
desktop.requestForeground( true );
return true;
}
@Override
public boolean show(final File file) {
if (!file.exists())
return false;
desktop.browseFileDirectory( file );
return true;
}
@Override
public boolean open(final URI url) {
try {
desktop.browse( url );
return true;
}
catch (final IOException e) {
logger.err( e, "While opening: %s", url );
return false;
}
}
}

View File

@@ -20,6 +20,6 @@
* @author lhunath, 2018-04-26
*/
@ParametersAreNonnullByDefault
package com.lyndir.masterpassword.gui.platform.mac;
package com.lyndir.masterpassword.gui.util.platform;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@@ -1,84 +0,0 @@
//==============================================================================
// This file is part of Master Password.
// Copyright (c) 2011-2017, Maarten Billemont.
//
// Master Password is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Master Password is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You can find a copy of the GNU General Public License in the
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
//==============================================================================
package com.lyndir.masterpassword.gui.view;
import com.google.common.collect.ImmutableList;
import com.lyndir.masterpassword.gui.Res;
import com.lyndir.masterpassword.gui.util.Components;
import com.lyndir.masterpassword.model.MPUser;
import java.awt.*;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.*;
/**
* @author lhunath, 2014-06-11
*/
public abstract class AuthenticationPanel<U extends MPUser<?>> extends Components.GradientPanel {
protected final UnlockFrame unlockFrame;
protected final JLabel avatarLabel;
protected AuthenticationPanel(final UnlockFrame unlockFrame) {
super( null, null );
this.unlockFrame = unlockFrame;
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
// Avatar
add( Box.createVerticalGlue() );
add( avatarLabel = new JLabel( Res.avatar( 0 ) ) {
@Override
public Dimension getMaximumSize() {
return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE );
}
} );
add( Box.createVerticalGlue() );
avatarLabel.setToolTipText( "The avatar for your user. Click to change it." );
}
protected void updateUser(final boolean repack) {
unlockFrame.updateUser( getSelectedUser() );
validate();
if (repack)
unlockFrame.repack();
}
@Nullable
protected abstract U getSelectedUser();
@Nonnull
public abstract char[] getMasterPassword();
@Nullable
public Component getFocusComponent() {
return null;
}
public Iterable<? extends JButton> getButtons() {
return ImmutableList.of();
}
public abstract void reset();
public abstract PasswordFrame<?, ?> newPasswordFrame();
}

View File

@@ -0,0 +1,70 @@
package com.lyndir.masterpassword.gui.view;
import static com.lyndir.masterpassword.util.Utilities.*;
import com.google.common.collect.ImmutableSortedSet;
import com.lyndir.masterpassword.gui.MasterPassword;
import com.lyndir.masterpassword.gui.util.*;
import com.lyndir.masterpassword.model.MPUser;
import com.lyndir.masterpassword.model.impl.MPFileUser;
import com.lyndir.masterpassword.model.impl.MPFileUserManager;
import java.awt.*;
import javax.annotation.Nullable;
import javax.swing.*;
/**
* @author lhunath, 2018-07-14
*/
@SuppressWarnings("serial")
public class FilesPanel extends JPanel implements MPFileUserManager.Listener, MasterPassword.Listener {
private final JButton avatarButton = Components.button( Res.icons().avatar( 0 ), event -> setAvatar(),
"Click to change the user's avatar." );
private final CollectionListModel<MPUser<?>> usersModel =
new CollectionListModel<MPUser<?>>( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser );
protected FilesPanel() {
setOpaque( false );
setBackground( Res.colors().transparent() );
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
// -
add( Box.createVerticalGlue() );
// Avatar
add( avatarButton );
avatarButton.setHorizontalAlignment( SwingConstants.CENTER );
avatarButton.setMaximumSize( new Dimension( Integer.MAX_VALUE, 0 ) );
// -
add( Components.strut( Components.margin() ) );
// User Selection
add( Components.comboBox( usersModel, user -> ifNotNull( user, MPUser::getFullName ) ) );
MPFileUserManager.get().addListener( this );
MasterPassword.get().addListener( this );
}
private void setAvatar() {
MPUser<?> selectedUser = usersModel.getSelectedItem();
if (selectedUser == null)
return;
selectedUser.setAvatar( (selectedUser.getAvatar() + 1) % Res.icons().avatars() );
avatarButton.setIcon( Res.icons().avatar( selectedUser.getAvatar() ) );
}
@Override
public void onFilesUpdated(final ImmutableSortedSet<MPFileUser> files) {
usersModel.set( files );
}
@Override
public void onUserSelected(@Nullable final MPUser<?> user) {
usersModel.setSelectedItem( user );
avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) );
}
}

View File

@@ -1,127 +0,0 @@
//==============================================================================
// This file is part of Master Password.
// Copyright (c) 2011-2017, Maarten Billemont.
//
// Master Password is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Master Password is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You can find a copy of the GNU General Public License in the
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
//==============================================================================
package com.lyndir.masterpassword.gui.view;
import com.google.common.base.Preconditions;
import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.MPAlgorithm;
import com.lyndir.masterpassword.MPResultType;
import com.lyndir.masterpassword.gui.Res;
import com.lyndir.masterpassword.gui.model.MPIncognitoSite;
import com.lyndir.masterpassword.gui.model.MPIncognitoUser;
import com.lyndir.masterpassword.gui.util.Components;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
/**
* @author lhunath, 2014-06-11
*/
@SuppressWarnings({ "serial", "MagicNumber" })
public class IncognitoAuthenticationPanel extends AuthenticationPanel<MPIncognitoUser> implements DocumentListener, ActionListener {
private final JTextField fullNameField;
private final JPasswordField masterPasswordField;
public IncognitoAuthenticationPanel(final UnlockFrame unlockFrame) {
// Full Name
super( unlockFrame );
add( Components.stud() );
JLabel fullNameLabel = Components.label( "Full Name:" );
add( fullNameLabel );
fullNameField = Components.textField();
fullNameField.setFont( Res.valueFont().deriveFont( 12f ) );
fullNameField.getDocument().addDocumentListener( this );
fullNameField.addActionListener( this );
add( fullNameField );
add( Components.stud() );
// Master Password
JLabel masterPasswordLabel = Components.label( "Master Password:" );
add( masterPasswordLabel );
masterPasswordField = Components.passwordField();
masterPasswordField.addActionListener( this );
masterPasswordField.getDocument().addDocumentListener( this );
add( masterPasswordField );
}
@Override
public Component getFocusComponent() {
return fullNameField;
}
@Override
public void reset() {
masterPasswordField.setText( "" );
}
@Override
public PasswordFrame<MPIncognitoUser, ?> newPasswordFrame() {
return new PasswordFrame<MPIncognitoUser, MPIncognitoSite>( Preconditions.checkNotNull( getSelectedUser() ) ) {
@Override
protected MPIncognitoSite createSite(final MPIncognitoUser user, final String siteName, final UnsignedInteger siteCounter,
final MPResultType resultType, final MPAlgorithm algorithm) {
return new MPIncognitoSite( user, siteName, algorithm, siteCounter, resultType, null );
}
};
}
@Nullable
@Override
protected MPIncognitoUser getSelectedUser() {
return new MPIncognitoUser( fullNameField.getText() );
}
@Nonnull
@Override
public char[] getMasterPassword() {
return masterPasswordField.getPassword();
}
@Override
public void insertUpdate(final DocumentEvent e) {
updateUser( false );
}
@Override
public void removeUpdate(final DocumentEvent e) {
updateUser( false );
}
@Override
public void changedUpdate(final DocumentEvent e) {
updateUser( false );
}
@Override
public void actionPerformed(final ActionEvent e) {
updateUser( false );
unlockFrame.trySignIn( fullNameField, masterPasswordField );
}
}

View File

@@ -0,0 +1,58 @@
package com.lyndir.masterpassword.gui.view;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.gui.util.Components;
import com.lyndir.masterpassword.gui.util.Res;
import com.lyndir.masterpassword.model.impl.MPFileUserManager;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import javax.swing.*;
import javax.swing.border.BevelBorder;
/**
* @author lhunath, 2018-07-14
*/
@SuppressWarnings("serial")
public class MasterPasswordFrame extends JFrame {
private static final Logger logger = Logger.get( MasterPasswordFrame.class );
private final UserContentPanel userContent;
@SuppressWarnings("MagicNumber")
public MasterPasswordFrame() {
super( "Master Password" );
JPanel root, userPanel;
setContentPane( root = Components.borderPanel( Res.colors().frameBg(), BoxLayout.PAGE_AXIS ) );
root.add( new FilesPanel() );
root.add( Components.strut() );
root.add( userPanel = Components.panel( new BorderLayout( 0, 0 ) ) );
userPanel.add( Components.borderPanel(
BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ),
Res.colors().controlBg(), BoxLayout.PAGE_AXIS, userContent = new UserContentPanel() ), BorderLayout.CENTER );
userPanel.add( userContent.getUserToolbar(), BorderLayout.LINE_START );
userPanel.add( userContent.getSiteToolbar(), BorderLayout.LINE_END );
addComponentListener( new ComponentHandler() );
setPreferredSize( new Dimension( 800, 560 ) );
setDefaultCloseOperation( DISPOSE_ON_CLOSE );
pack();
setLocationRelativeTo( null );
setLocationByPlatform( true );
}
private class ComponentHandler extends ComponentAdapter {
@Override
public void componentShown(final ComponentEvent e) {
MPFileUserManager.get().reload();
userContent.transferFocus();
}
}
}

View File

@@ -1,248 +0,0 @@
//==============================================================================
// This file is part of Master Password.
// Copyright (c) 2011-2017, Maarten Billemont.
//
// Master Password is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Master Password is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You can find a copy of the GNU General Public License in the
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
//==============================================================================
package com.lyndir.masterpassword.gui.view;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.UnsignedInteger;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.MPAlgorithm;
import com.lyndir.masterpassword.MPResultType;
import com.lyndir.masterpassword.gui.Res;
import com.lyndir.masterpassword.gui.util.Components;
import com.lyndir.masterpassword.model.MPUser;
import com.lyndir.masterpassword.model.impl.*;
import java.awt.*;
import java.awt.event.*;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.plaf.metal.MetalComboBoxEditor;
/**
* @author lhunath, 2014-06-11
*/
public class ModelAuthenticationPanel extends AuthenticationPanel<MPFileUser> implements ItemListener, ActionListener, DocumentListener {
@SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( ModelAuthenticationPanel.class );
private static final long serialVersionUID = 1L;
private final JComboBox<MPFileUser> userField;
private final JLabel masterPasswordLabel;
private final JPasswordField masterPasswordField;
public ModelAuthenticationPanel(final UnlockFrame unlockFrame) {
super( unlockFrame );
add( Components.stud() );
// Avatar
avatarLabel.addMouseListener( new MouseAdapter() {
@Override
public void mouseClicked(final MouseEvent e) {
MPFileUser selectedUser = getSelectedUser();
if (selectedUser != null) {
selectedUser.setAvatar( selectedUser.getAvatar() + 1 );
updateUser( false );
}
}
} );
// User
JLabel userLabel = Components.label( "User:" );
add( userLabel );
userField = Components.comboBox( readConfigUsers() );
userField.setFont( Res.valueFont().deriveFont( userField.getFont().getSize2D() ) );
userField.addItemListener( this );
userField.addActionListener( this );
userField.setRenderer( new DefaultListCellRenderer() {
private static final long serialVersionUID = 1L;
@Override
@SuppressWarnings("unchecked")
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
final boolean isSelected, final boolean cellHasFocus) {
String userValue = ((MPUser<MPFileSite>) value).getFullName();
return super.getListCellRendererComponent( list, userValue, index, isSelected, cellHasFocus );
}
} );
userField.setEditor( new MetalComboBoxEditor() {
@Override
protected JTextField createEditorComponent() {
JTextField editorComponents = Components.textField();
editorComponents.setForeground( Color.red );
return editorComponents;
}
} );
add( userField );
add( Components.stud() );
// Master Password
masterPasswordLabel = Components.label( "Master Password:" );
add( masterPasswordLabel );
masterPasswordField = Components.passwordField();
masterPasswordField.addActionListener( this );
masterPasswordField.getDocument().addDocumentListener( this );
add( masterPasswordField );
}
@Override
public Component getFocusComponent() {
return masterPasswordField.isVisible()? masterPasswordField: null;
}
@Override
protected void updateUser(boolean repack) {
MPFileUser selectedUser = getSelectedUser();
if (selectedUser != null) {
avatarLabel.setIcon( Res.avatar( selectedUser.getAvatar() ) );
boolean showPasswordField = !selectedUser.isMasterKeyAvailable(); // TODO: is this the same as keySaved()?
if (masterPasswordField.isVisible() != showPasswordField) {
masterPasswordLabel.setVisible( showPasswordField );
masterPasswordField.setVisible( showPasswordField );
repack = true;
}
}
super.updateUser( repack );
}
@Nullable
@Override
protected MPFileUser getSelectedUser() {
int selectedIndex = userField.getSelectedIndex();
if (selectedIndex < 0)
return null;
return userField.getModel().getElementAt( selectedIndex );
}
@Nonnull
@Override
public char[] getMasterPassword() {
return masterPasswordField.getPassword();
}
@Override
public Iterable<? extends JButton> getButtons() {
return ImmutableList.of( new JButton( Res.iconAdd() ) {
{
addActionListener( new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
String fullName = JOptionPane.showInputDialog( ModelAuthenticationPanel.this, //
"Enter your full name, ensuring it is correctly spelled and capitalized:",
"New User", JOptionPane.QUESTION_MESSAGE );
MPFileUserManager.get().addUser( new MPFileUser( fullName ) );
userField.setModel( new DefaultComboBoxModel<>( readConfigUsers() ) );
updateUser( true );
}
} );
setToolTipText( "Add a new user to the list." );
}
}, new JButton( Res.iconDelete() ) {
{
addActionListener( new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
MPFileUser deleteUser = getSelectedUser();
if (deleteUser == null)
return;
if (JOptionPane.showConfirmDialog( ModelAuthenticationPanel.this, //
strf( "Are you sure you want to delete the user and sites remembered for:%n%s.",
deleteUser.getFullName() ), //
"Delete User", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE )
== JOptionPane.CANCEL_OPTION)
return;
MPFileUserManager.get().deleteUser( deleteUser );
userField.setModel( new DefaultComboBoxModel<>( readConfigUsers() ) );
updateUser( true );
}
} );
setToolTipText( "Delete the selected user." );
}
}, new JButton( Res.iconQuestion() ) {
{
addActionListener( e -> JOptionPane.showMessageDialog(
ModelAuthenticationPanel.this, //
strf( "Reads users and sites from the directory at:%n%s",
MPFileUserManager.get().getPath().getAbsolutePath() ), //
"Help", JOptionPane.INFORMATION_MESSAGE ) );
setToolTipText( "More information." );
}
} );
}
@Override
public void reset() {
masterPasswordField.setText( "" );
}
@Override
public PasswordFrame<MPFileUser, MPFileSite> newPasswordFrame() {
return new PasswordFrame<MPFileUser, MPFileSite>( Preconditions.checkNotNull( getSelectedUser() ) ) {
@Override
protected MPFileSite createSite(final MPFileUser user, final String siteName, final UnsignedInteger siteCounter,
final MPResultType resultType,
final MPAlgorithm algorithm) {
return new MPFileSite( user, siteName, algorithm, siteCounter, resultType );
}
};
}
private static MPFileUser[] readConfigUsers() {
return MPFileUserManager.get().getUsers().toArray( new MPFileUser[0] );
}
@Override
public void itemStateChanged(final ItemEvent e) {
updateUser( false );
}
@Override
public void actionPerformed(final ActionEvent e) {
updateUser( false );
unlockFrame.trySignIn( userField );
}
@Override
public void insertUpdate(final DocumentEvent e) {
updateUser( false );
}
@Override
public void removeUpdate(final DocumentEvent e) {
updateUser( false );
}
@Override
public void changedUpdate(final DocumentEvent e) {
updateUser( false );
}
}

View File

@@ -1,279 +0,0 @@
//==============================================================================
// This file is part of Master Password.
// Copyright (c) 2011-2017, Maarten Billemont.
//
// Master Password is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Master Password is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You can find a copy of the GNU General Public License in the
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
//==============================================================================
package com.lyndir.masterpassword.gui.view;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.collect.Iterables;
import com.google.common.primitives.UnsignedInteger;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.gui.Res;
import com.lyndir.masterpassword.gui.util.*;
import com.lyndir.masterpassword.model.MPSite;
import com.lyndir.masterpassword.model.MPUser;
import com.lyndir.masterpassword.model.impl.MPFileSite;
import com.lyndir.masterpassword.model.impl.MPFileUser;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.WindowEvent;
import java.util.Collection;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
/**
* @author lhunath, 2014-06-08
*/
public abstract class PasswordFrame<U extends MPUser<S>, S extends MPSite<?>> extends JFrame implements DocumentListener {
private static final Logger logger = Logger.get( PasswordFrame.class );
@SuppressWarnings("FieldCanBeLocal")
private final Components.GradientPanel root;
private final JTextField siteNameField;
private final JButton siteActionButton;
private final JComboBox<MPAlgorithm.Version> siteVersionField;
private final JSpinner siteCounterField;
private final UnsignedIntegerModel siteCounterModel;
private final JComboBox<MPResultType> resultTypeField;
private final JPasswordField passwordField;
private final JLabel tipLabel;
private final JCheckBox maskPasswordField;
private final char passwordEchoChar;
private final Font passwordEchoFont;
private final U user;
@Nullable
private S currentSite;
private boolean updatingUI;
@SuppressWarnings("MagicNumber")
protected PasswordFrame(final U user) {
super( "Master Password" );
this.user = user;
setDefaultCloseOperation( DISPOSE_ON_CLOSE );
setContentPane( root = Components.gradientPanel( new FlowLayout(), Res.colors().frameBg() ) );
root.setLayout( new BoxLayout( root, BoxLayout.PAGE_AXIS ) );
root.setBorder( BorderFactory.createEmptyBorder( 20, 20, 20, 20 ) );
// Site
JPanel sitePanel = Components.boxLayout( BoxLayout.PAGE_AXIS );
sitePanel.setOpaque( true );
sitePanel.setBackground( Res.colors().controlBg() );
sitePanel.setBorder( BorderFactory.createEmptyBorder( 20, 20, 20, 20 ) );
root.add( Components.borderPanel( sitePanel, BorderFactory.createRaisedBevelBorder(), Res.colors().frameBg() ) );
// User
sitePanel.add( Components.label( strf( "Generating passwords for: %s", user.getFullName() ), SwingConstants.CENTER ) );
sitePanel.add( Components.stud() );
// Site Name
sitePanel.add( Components.label( "Site Name:" ) );
JComponent siteControls = Components.boxLayout( BoxLayout.LINE_AXIS, //
siteNameField = Components.textField(), Components.stud(),
siteActionButton = Components.button( "Add Site" ) );
siteNameField.getDocument().addDocumentListener( this );
siteNameField.addActionListener(
e -> Futures.addCallback( updatePassword( true ), new FailableCallback<String>( logger ) {
@Override
public void onSuccess(@Nullable final String sitePassword) {
if (sitePassword == null)
return;
if (currentSite instanceof MPFileSite)
((MPFileSite) currentSite).use();
Transferable clipboardContents = new StringSelection( sitePassword );
Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null );
dispatchEvent( new WindowEvent( PasswordFrame.this, WindowEvent.WINDOW_CLOSING ) );
}
}, Res.uiExecutor( false ) ) );
siteActionButton.addActionListener(
e -> {
if (currentSite == null)
return;
if (currentSite instanceof MPFileSite)
this.user.deleteSite( currentSite );
else
this.user.addSite( currentSite );
siteNameField.requestFocus();
updatePassword( true );
} );
sitePanel.add( siteControls );
sitePanel.add( Components.stud() );
// Site Type & Counter
siteCounterModel = new UnsignedIntegerModel( UnsignedInteger.ONE, UnsignedInteger.ONE );
MPResultType[] types = Iterables.toArray( MPResultType.forClass( MPResultTypeClass.Template ), MPResultType.class );
JComponent siteSettings = Components.boxLayout( BoxLayout.LINE_AXIS, //
resultTypeField = Components.comboBox( types ), //
Components.stud(), //
siteVersionField = Components.comboBox( MPAlgorithm.Version.values() ), //
Components.stud(), //
siteCounterField = Components.spinner( siteCounterModel ) );
sitePanel.add( siteSettings );
resultTypeField.setFont( Res.valueFont().deriveFont( resultTypeField.getFont().getSize2D() ) );
resultTypeField.setSelectedItem( user.getAlgorithm().mpw_default_result_type() );
resultTypeField.addItemListener( e -> updatePassword( true ) );
siteVersionField.setFont( Res.valueFont().deriveFont( siteVersionField.getFont().getSize2D() ) );
siteVersionField.setAlignmentX( RIGHT_ALIGNMENT );
siteVersionField.setSelectedItem( user.getAlgorithm() );
siteVersionField.addItemListener( e -> updatePassword( true ) );
siteCounterField.setFont( Res.valueFont().deriveFont( siteCounterField.getFont().getSize2D() ) );
siteCounterField.setAlignmentX( RIGHT_ALIGNMENT );
siteCounterField.addChangeListener( e -> updatePassword( true ) );
// Mask
maskPasswordField = Components.checkBox( "Hide Password" );
maskPasswordField.setAlignmentX( Component.CENTER_ALIGNMENT );
maskPasswordField.setSelected( true );
maskPasswordField.addItemListener( e -> updateMask() );
// Password
passwordField = Components.passwordField();
passwordField.setAlignmentX( Component.CENTER_ALIGNMENT );
passwordField.setHorizontalAlignment( SwingConstants.CENTER );
passwordField.putClientProperty( "JPasswordField.cutCopyAllowed", true );
passwordField.setEditable( false );
passwordField.setBackground( null );
passwordField.setBorder( null );
passwordEchoChar = passwordField.getEchoChar();
passwordEchoFont = passwordField.getFont().deriveFont( 40f );
updateMask();
// Tip
tipLabel = Components.label( " ", SwingConstants.CENTER );
tipLabel.setAlignmentX( Component.CENTER_ALIGNMENT );
JPanel passwordContainer = Components.boxLayout( BoxLayout.PAGE_AXIS, maskPasswordField, Box.createGlue(), passwordField,
Box.createGlue(), tipLabel );
passwordContainer.setOpaque( true );
passwordContainer.setBackground( Color.white );
passwordContainer.setBorder( BorderFactory.createEmptyBorder( 8, 8, 8, 8 ) );
root.add( Box.createVerticalStrut( 8 ) );
root.add( Components.borderPanel( passwordContainer, BorderFactory.createLoweredSoftBevelBorder(), Res.colors().frameBg() ),
BorderLayout.SOUTH );
pack();
setMinimumSize( new Dimension( Math.max( 600, getPreferredSize().width ), Math.max( 300, getPreferredSize().height ) ) );
pack();
setLocationByPlatform( true );
setLocationRelativeTo( null );
}
@SuppressWarnings("MagicNumber")
private void updateMask() {
passwordField.setEchoChar( maskPasswordField.isSelected()? passwordEchoChar: (char) 0 );
passwordField.setFont( maskPasswordField.isSelected()? passwordEchoFont: Res.bigValueFont().deriveFont( 40f ) );
}
@Nonnull
private ListenableFuture<String> updatePassword(final boolean allowNameCompletion) {
String siteNameQuery = siteNameField.getText();
if (updatingUI)
return Futures.immediateCancelledFuture();
if ((siteNameQuery == null) || siteNameQuery.isEmpty() || !user.isMasterKeyAvailable()) {
siteActionButton.setVisible( false );
tipLabel.setText( null );
passwordField.setText( null );
return Futures.immediateCancelledFuture();
}
MPResultType resultType = resultTypeField.getModel().getElementAt( resultTypeField.getSelectedIndex() );
MPAlgorithm siteAlgorithm = siteVersionField.getItemAt( siteVersionField.getSelectedIndex() ).getAlgorithm();
UnsignedInteger siteCounter = siteCounterModel.getNumber();
Collection<S> siteResults = user.findSites( siteNameQuery );
if (!allowNameCompletion)
siteResults = siteResults.stream().filter(
siteResult -> (siteResult != null) && siteNameQuery.equals( siteResult.getName() ) ).collect( Collectors.toList() );
S site = ifNotNullElse( Iterables.getFirst( siteResults, null ),
createSite( user, siteNameQuery, siteCounter, resultType, siteAlgorithm ) );
if ((currentSite != null) && currentSite.getName().equals( site.getName() )) {
site.setResultType( resultType );
site.setAlgorithm( siteAlgorithm );
site.setCounter( siteCounter );
}
ListenableFuture<String> passwordFuture = Res.job( this, () ->
site.getResult( MPKeyPurpose.Authentication, null, null ) );
SwingUtilities.invokeLater( () -> {
updatingUI = true;
currentSite = site;
siteActionButton.setVisible( user instanceof MPFileUser );
if (currentSite instanceof MPFileSite)
siteActionButton.setText( "Delete Site" );
else
siteActionButton.setText( "Add Site" );
resultTypeField.setSelectedItem( currentSite.getResultType() );
siteVersionField.setSelectedItem( currentSite.getAlgorithm().version() );
siteCounterField.setValue( currentSite.getCounter() );
siteNameField.setText( currentSite.getName() );
if (siteNameField.getText().startsWith( siteNameQuery ))
siteNameField.select( siteNameQuery.length(), siteNameField.getText().length() );
passwordField.setText( null );
tipLabel.setText( "Getting password..." );
Futures.addCallback( passwordFuture, new FailableCallback<String>( logger ) {
@Override
public void onSuccess(@Nullable final String sitePassword) {
if (sitePassword != null)
tipLabel.setText( "Press [Enter] to copy the password. Then paste it into the password field." );
passwordField.setText( sitePassword );
updatingUI = false;
}
}, Res.uiExecutor( true ) );
} );
return passwordFuture;
}
protected abstract S createSite(U user, String siteName, UnsignedInteger siteCounter, MPResultType resultType, MPAlgorithm algorithm);
@Override
public void insertUpdate(final DocumentEvent e) {
updatePassword( true );
}
@Override
public void removeUpdate(final DocumentEvent e) {
updatePassword( false );
}
@Override
public void changedUpdate(final DocumentEvent e) {
updatePassword( true );
}
}

View File

@@ -1,216 +0,0 @@
//==============================================================================
// This file is part of Master Password.
// Copyright (c) 2011-2017, Maarten Billemont.
//
// Master Password is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Master Password is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You can find a copy of the GNU General Public License in the
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
//==============================================================================
package com.lyndir.masterpassword.gui.view;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.lyndir.masterpassword.MPAlgorithmException;
import com.lyndir.masterpassword.MPIdenticon;
import com.lyndir.masterpassword.gui.Res;
import com.lyndir.masterpassword.gui.util.Components;
import com.lyndir.masterpassword.model.*;
import java.awt.*;
import java.awt.event.*;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.swing.*;
/**
* @author lhunath, 2014-06-08
*/
@SuppressWarnings({ "MagicNumber", "serial" })
public class UnlockFrame extends JFrame {
private final SignInCallback signInCallback;
private final Components.GradientPanel root;
private final JLabel identiconLabel;
private final JButton signInButton;
private final JPanel authenticationContainer;
private AuthenticationPanel<?> authenticationPanel;
private Future<?> identiconFuture;
private boolean incognito;
@Nullable
private MPUser<? extends MPSite<?>> user;
public UnlockFrame(final SignInCallback signInCallback) {
super( "Unlock Master Password" );
this.signInCallback = signInCallback;
setDefaultCloseOperation( DISPOSE_ON_CLOSE );
addWindowFocusListener( new WindowAdapter() {
@Override
public void windowGainedFocus(final WindowEvent e) {
root.setGradientColor( Res.colors().frameBg() );
}
@Override
public void windowLostFocus(final WindowEvent e) {
root.setGradientColor( Color.RED );
}
} );
// Sign In
JPanel signInBox = Components.boxLayout( BoxLayout.LINE_AXIS, Box.createGlue(), signInButton = Components.button( "Sign In" ),
Box.createGlue() );
signInBox.setBackground( null );
setContentPane( root = Components.gradientPanel( new FlowLayout(), Res.colors().frameBg() ) );
root.setLayout( new BoxLayout( root, BoxLayout.PAGE_AXIS ) );
root.setBorder( BorderFactory.createEmptyBorder( 20, 20, 20, 20 ) );
root.add( Components.borderPanel( authenticationContainer = Components.boxLayout( BoxLayout.PAGE_AXIS ),
BorderFactory.createRaisedBevelBorder(), Res.colors().frameBg() ) );
root.add( Box.createVerticalStrut( 8 ) );
root.add( identiconLabel = Components.label( " ", SwingConstants.CENTER ) );
root.add( Box.createVerticalStrut( 8 ) );
root.add( signInBox );
authenticationContainer.setOpaque( true );
authenticationContainer.setBackground( Res.colors().controlBg() );
authenticationContainer.setBorder( BorderFactory.createEmptyBorder( 20, 20, 20, 20 ) );
identiconLabel.setFont( Res.emoticonsFont().deriveFont( 14.f ) );
identiconLabel.setToolTipText(
strf( "A representation of your identity across all Master Password apps.%nIt should always be the same." ) );
signInButton.addActionListener( new AbstractAction() {
@Override
public void actionPerformed(final ActionEvent e) {
trySignIn();
}
} );
createAuthenticationPanel();
setLocationByPlatform( true );
setLocationRelativeTo( null );
}
protected void repack() {
pack();
setMinimumSize( new Dimension( Math.max( 300, getPreferredSize().width ), Math.max( 300, getPreferredSize().height ) ) );
pack();
}
private void createAuthenticationPanel() {
authenticationContainer.removeAll();
if (incognito) {
authenticationPanel = new IncognitoAuthenticationPanel( this );
} else {
authenticationPanel = new ModelAuthenticationPanel( this );
}
authenticationPanel.updateUser( false );
authenticationContainer.add( authenticationPanel );
authenticationContainer.add( Components.stud() );
JCheckBox incognitoCheckBox = Components.checkBox( "Incognito" );
incognitoCheckBox.setToolTipText( "Log in without saving any information." );
incognitoCheckBox.setSelected( incognito );
incognitoCheckBox.addItemListener( e -> {
incognito = incognitoCheckBox.isSelected();
SwingUtilities.invokeLater( this::createAuthenticationPanel );
} );
JComponent toolsPanel = Components.boxLayout( BoxLayout.LINE_AXIS, incognitoCheckBox, Box.createGlue() );
authenticationContainer.add( toolsPanel );
for (final JButton button : authenticationPanel.getButtons()) {
toolsPanel.add( button );
button.setBorder( BorderFactory.createEmptyBorder() );
button.setMargin( new Insets( 0, 0, 0, 0 ) );
button.setAlignmentX( RIGHT_ALIGNMENT );
button.setContentAreaFilled( false );
}
checkSignIn();
validate();
repack();
SwingUtilities.invokeLater( () -> ifNotNullElse( authenticationPanel.getFocusComponent(), signInButton ).requestFocusInWindow() );
}
void updateUser(@Nullable final MPUser<? extends MPSite<?>> user) {
this.user = user;
checkSignIn();
}
boolean checkSignIn() {
if (identiconFuture != null)
identiconFuture.cancel( false );
identiconFuture = Res.job( this, () -> SwingUtilities.invokeLater( () -> {
String fullName = (user == null)? "": user.getFullName();
char[] masterPassword = authenticationPanel.getMasterPassword();
if (fullName.isEmpty() || (masterPassword.length == 0)) {
identiconLabel.setText( " " );
return;
}
MPIdenticon identicon = new MPIdenticon( fullName, masterPassword );
identiconLabel.setText( identicon.getText() );
identiconLabel.setForeground(
Res.colors().fromIdenticonColor( identicon.getColor(), Res.Colors.BackgroundMode.DARK ) );
} ), 300, TimeUnit.MILLISECONDS );
String fullName = (user == null)? "": user.getFullName();
char[] masterPassword = authenticationPanel.getMasterPassword();
boolean enabled = !fullName.isEmpty() && (masterPassword.length > 0);
signInButton.setEnabled( enabled );
return enabled;
}
void trySignIn(final JComponent... signInComponents) {
if ((user == null) || !checkSignIn())
return;
for (final JComponent signInComponent : signInComponents)
signInComponent.setEnabled( false );
signInButton.setEnabled( false );
signInButton.setText( "Signing In..." );
Res.job( this, () -> {
try {
user.authenticate( authenticationPanel.getMasterPassword() );
SwingUtilities.invokeLater( () -> {
signInCallback.signedIn( authenticationPanel.newPasswordFrame() );
dispose();
} );
}
catch (final MPIncorrectMasterPasswordException | MPAlgorithmException e) {
SwingUtilities.invokeLater( () -> {
JOptionPane.showMessageDialog( null, e.getLocalizedMessage(), "Sign In Failed", JOptionPane.ERROR_MESSAGE );
authenticationPanel.reset();
signInButton.setText( "Sign In" );
for (final JComponent signInComponent : signInComponents)
signInComponent.setEnabled( true );
checkSignIn();
} );
}
} );
}
@FunctionalInterface
public interface SignInCallback {
void signedIn(PasswordFrame<?, ?> passwordFrame);
}
}

View File

@@ -0,0 +1,987 @@
package com.lyndir.masterpassword.gui.view;
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
import com.google.common.base.*;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.UnsignedInteger;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.lhunath.opal.system.util.ObjectUtils;
import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.gui.MPGuiConstants;
import com.lyndir.masterpassword.gui.MasterPassword;
import com.lyndir.masterpassword.gui.model.*;
import com.lyndir.masterpassword.gui.util.*;
import com.lyndir.masterpassword.gui.util.Platform;
import com.lyndir.masterpassword.model.*;
import com.lyndir.masterpassword.model.impl.*;
import com.lyndir.masterpassword.util.Utilities;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.Optional;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.PlainDocument;
/**
* @author lhunath, 2018-07-14
*/
@SuppressWarnings("SerializableStoresNonSerializable")
public class UserContentPanel extends JPanel implements MasterPassword.Listener, MPUser.Listener {
private static final Random random = new Random();
private static final int SIZE_RESULT = 48;
private static final Logger logger = Logger.get( UserContentPanel.class );
private static final JButton iconButton = Components.button( Res.icons().user(), null, null );
private static final KeyStroke copyLoginKeyStroke = KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK );
private final JButton addButton = Components.button( Res.icons().add(), event -> addUser(),
"Add a new user to Master Password." );
private final JButton importButton = Components.button( Res.icons().import_(), event -> importUser(),
"Import a user from a backup file into Master Password." );
private final JButton helpButton = Components.button( Res.icons().help(), event -> showHelp(),
"Show information on how to use Master Password." );
private final JPanel userToolbar = Components.panel( BoxLayout.PAGE_AXIS );
private final JPanel siteToolbar = Components.panel( BoxLayout.PAGE_AXIS );
@Nullable
private MPUser<?> showingUser;
private ContentMode contentMode;
public UserContentPanel() {
userToolbar.setPreferredSize( iconButton.getPreferredSize() );
siteToolbar.setPreferredSize( iconButton.getPreferredSize() );
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
setBorder( Components.marginBorder() );
showUser( null );
MasterPassword.get().addListener( this );
}
protected JComponent getUserToolbar() {
return userToolbar;
}
protected JComponent getSiteToolbar() {
return siteToolbar;
}
@Override
public void onUserSelected(@Nullable final MPUser<?> user) {
showUser( user );
}
@Override
public void onUserUpdated(final MPUser<?> user) {
showUser( user );
}
@Override
public void onUserAuthenticated(final MPUser<?> user) {
showUser( user );
}
@Override
public void onUserInvalidated(final MPUser<?> user) {
showUser( user );
}
private void showUser(@Nullable final MPUser<?> user) {
Res.ui( () -> {
if (showingUser != null)
showingUser.removeListener( this );
ContentMode newContentMode = ContentMode.getContentMode( user );
if ((newContentMode != contentMode) || !ObjectUtils.equals( showingUser, user )) {
userToolbar.removeAll();
siteToolbar.removeAll();
removeAll();
showingUser = user;
switch (contentMode = newContentMode) {
case NO_USER:
add( new NoUserPanel() );
break;
case AUTHENTICATE:
add( new AuthenticateUserPanel( Preconditions.checkNotNull( showingUser ) ) );
break;
case AUTHENTICATED:
add( new AuthenticatedUserPanel( Preconditions.checkNotNull( showingUser ) ) );
break;
}
revalidate();
transferFocus();
}
if (showingUser != null)
showingUser.addListener( this );
} );
}
private void addUser() {
JTextField nameField = Components.textField( "Robert Lee Mitchell", null );
JCheckBox incognitoField = Components.checkBox( "<html>Incognito <em>(Do not save this user to disk)</em></html>" );
if (JOptionPane.OK_OPTION != Components.showDialog( this, "Add User", new JOptionPane( Components.panel(
BoxLayout.PAGE_AXIS,
Components.label( "<html>Enter your full legal name:</html>" ),
Components.strut(),
nameField,
Components.strut(),
incognitoField ), JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION ) {
@Override
public void selectInitialValue() {
nameField.requestFocusInWindow();
}
} ))
return;
String fullName = nameField.getText();
if (Strings.isNullOrEmpty( fullName ))
return;
if (incognitoField.isSelected())
MasterPassword.get().activateUser( new MPIncognitoUser( fullName ) );
else
MasterPassword.get().activateUser( MPFileUserManager.get().add( fullName ) );
}
private void importUser() {
File importFile = Components.showLoadDialog( this, "Import User File" );
if (importFile == null)
return;
try {
MPFileUser importUser = MPFileUser.load( importFile );
if (importUser == null) {
JOptionPane.showMessageDialog(
this, "Not a Master Password file.",
"Import Failed", JOptionPane.ERROR_MESSAGE );
return;
}
JPasswordField passwordField = Components.passwordField();
if (JOptionPane.OK_OPTION == Components.showDialog( this, "Import User", new JOptionPane( Components.panel(
BoxLayout.PAGE_AXIS,
Components.label( strf( "<html>Enter the master password to import <strong>%s</strong>:</html>",
importUser.getFullName() ) ),
Components.strut(),
passwordField ), JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION ) {
@Override
public void selectInitialValue() {
passwordField.requestFocusInWindow();
}
} )) {
try {
importUser.authenticate( passwordField.getPassword() );
Optional<MPFileUser> existingUser = MPFileUserManager.get().getFiles().stream().filter(
user -> user.getFullName().equalsIgnoreCase( importUser.getFullName() ) ).findFirst();
if (existingUser.isPresent() && (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(
this,
strf( "<html>Importing user <strong>%s</strong> from this file will replace the existing user with the imported one.<br>"
+ "Are you sure?<br><br>"
+ "<em>Existing user last modified: %s<br>Imported user last modified: %s</em></html>",
importUser.getFullName(),
Res.format( existingUser.get().getLastUsed() ),
Res.format( importUser.getLastUsed() ) ) )))
return;
MasterPassword.get().activateUser( MPFileUserManager.get().add( importUser ) );
}
catch (final MPIncorrectMasterPasswordException | MPAlgorithmException e) {
JOptionPane.showMessageDialog(
this, e.getLocalizedMessage(),
"Import Failed", JOptionPane.ERROR_MESSAGE );
}
}
}
catch (final IOException e) {
logger.err( e, "While reading user import file." );
JOptionPane.showMessageDialog(
this, strf( "<html>Couldn't read import file:<br><pre>%s</pre></html>.", e.getLocalizedMessage() ),
"Import Failed", JOptionPane.ERROR_MESSAGE );
}
catch (final MPMarshalException e) {
logger.err( e, "While parsing user import file." );
JOptionPane.showMessageDialog(
this, strf( "<html>Couldn't parse import file:<br><pre>%s</pre></html>.", e.getLocalizedMessage() ),
"Import Failed", JOptionPane.ERROR_MESSAGE );
}
}
private void showHelp() {
JOptionPane.showMessageDialog( this, Components.linkLabel( strf(
"<h1>Master Password - v%s</h1>"
+ "<p>The primary goal of this application is to provide a reliable security solution that also "
+ "makes you independent from your computer. If you lose access to this computer or your data, "
+ "the application can regenerate all your secrets from scratch on any new device.</p>"
+ "<h2>Opening Master Password</h2>"
+ "<p>To use Master Password, simply open the application on your computer. "
+ "Once running, you can bring up the user interface at any time by pressing the keys "
+ "<strong><code>%s+%s</code></strong>."
+ "<h2>Persistence</h2>"
+ "<p>Though at the core, Master Password does not require the use of any form of data "
+ "storage, the application does remember the names of the sites you've used in the past to "
+ "make it easier for you to use them again in the future. All user information is saved in "
+ "files on your computer at the following location:<br><pre>%s</pre></p>"
+ "<p>You can read, modify, backup or place new files in this location as you see fit. "
+ "Some people even configure this location to be synced between their different computers "
+ "using services such as those provided by SpiderOak or Dropbox.</p>"
+ "<hr><p><a href='https://masterpassword.app'>https://masterpassword.app</a> — by Maarten Billemont</p>",
MasterPassword.get().version(),
InputEvent.getModifiersExText( MPGuiConstants.ui_hotkey.getModifiers() ),
KeyEvent.getKeyText( MPGuiConstants.ui_hotkey.getKeyCode() ),
MPFileUserManager.get().getPath().getAbsolutePath() ) ),
"About Master Password", JOptionPane.INFORMATION_MESSAGE );
}
private enum ContentMode {
NO_USER,
AUTHENTICATE,
AUTHENTICATED;
static ContentMode getContentMode(@Nullable final MPUser<?> user) {
if (user == null)
return NO_USER;
else if (!user.isMasterKeyAvailable())
return AUTHENTICATE;
else
return AUTHENTICATED;
}
}
private final class NoUserPanel extends JPanel {
private NoUserPanel() {
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
userToolbar.add( addButton );
userToolbar.add( importButton );
userToolbar.add( Box.createGlue() );
userToolbar.add( helpButton );
add( Box.createGlue() );
add( Components.heading( "Select a user to proceed." ) );
add( Box.createGlue() );
}
}
private final class AuthenticateUserPanel extends JPanel implements ActionListener, DocumentListener {
@Nonnull
private final MPUser<?> user;
private final JButton exportButton = Components.button( Res.icons().export(), event -> exportUser(),
"Export this user to a backup file." );
private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteUser(),
"Delete this user from Master Password." );
private final JButton resetButton = Components.button( Res.icons().reset(), event -> resetUser(),
"Change the master password for this user." );
private final JPasswordField masterPasswordField;
private final JLabel errorLabel;
private final JLabel identiconLabel;
private Future<?> identiconJob;
private AuthenticateUserPanel(@Nonnull final MPUser<?> user) {
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
this.user = user;
userToolbar.add( addButton );
userToolbar.add( importButton );
userToolbar.add( exportButton );
userToolbar.add( deleteButton );
userToolbar.add( resetButton );
userToolbar.add( Box.createGlue() );
userToolbar.add( helpButton );
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
add( Components.strut() );
add( identiconLabel = Components.label( SwingConstants.CENTER ) );
identiconLabel.setFont( Res.fonts().emoticonsFont( Components.TEXT_SIZE_CONTROL ) );
add( Box.createGlue() );
add( Components.label( "Master Password:" ) );
add( Components.strut() );
add( masterPasswordField = Components.passwordField() );
masterPasswordField.addActionListener( this );
masterPasswordField.getDocument().addDocumentListener( this );
add( errorLabel = Components.label() );
errorLabel.setForeground( Res.colors().errorFg() );
add( Box.createGlue() );
}
private void exportUser() {
MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null;
if (fileUser == null)
return;
File exportFile = Components.showSaveDialog( this, "Export User File", fileUser.getFile().getName() );
if (exportFile == null)
return;
try {
Platform.get().show(
Files.copy( fileUser.getFile().toPath(), exportFile.toPath(),
StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES ).toFile() );
}
catch (final IOException e) {
JOptionPane.showMessageDialog(
this, e.getLocalizedMessage(),
"Export Failed", JOptionPane.ERROR_MESSAGE );
}
}
private void deleteUser() {
MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null;
if (fileUser == null)
return;
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
SwingUtilities.windowForComponent( this ), strf( "<html>Delete the user <strong>%s</strong>?<br><br><em>%s</em></html>",
fileUser.getFullName(), fileUser.getFile().getName() ),
"Delete User", JOptionPane.YES_NO_OPTION ))
MPFileUserManager.get().delete( fileUser );
}
private void resetUser() {
JPasswordField passwordField = Components.passwordField();
if (JOptionPane.OK_OPTION == Components.showDialog( this, "Reset User", new JOptionPane( Components.panel(
BoxLayout.PAGE_AXIS,
Components.label( strf( "<html>Enter the new master password for <strong>%s</strong>:</html>",
user.getFullName() ) ),
Components.strut(),
passwordField,
Components.strut(),
Components.label( strf( "<html><em><strong>Note:</strong><br>Changing the master password "
+ "will change all of the user's passwords.<br>"
+ "Changing back to the original master password will also restore<br>"
+ "the user's original passwords.</em></html>",
user.getFullName() ) ) ), JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION ) {
@Override
public void selectInitialValue() {
passwordField.requestFocusInWindow();
}
} )) {
char[] masterPassword = passwordField.getPassword();
if ((masterPassword != null) && (masterPassword.length > 0))
try {
user.reset();
user.authenticate( masterPassword );
}
catch (final MPIncorrectMasterPasswordException e) {
errorLabel.setText( e.getLocalizedMessage() );
throw logger.bug( e );
}
catch (final MPAlgorithmException e) {
logger.err( e, "While resetting master password." );
errorLabel.setText( e.getLocalizedMessage() );
}
}
}
@Override
public void actionPerformed(final ActionEvent event) {
updateIdenticon();
char[] masterPassword = masterPasswordField.getPassword();
Res.job( () -> {
try {
user.authenticate( masterPassword );
}
catch (final MPIncorrectMasterPasswordException e) {
logger.wrn( e, "During user authentication for: %s", user );
errorLabel.setText( e.getLocalizedMessage() );
}
catch (final MPAlgorithmException e) {
logger.err( e, "During user authentication for: %s", user );
errorLabel.setText( e.getLocalizedMessage() );
}
} );
}
@Override
public void insertUpdate(final DocumentEvent event) {
update();
}
@Override
public void removeUpdate(final DocumentEvent event) {
update();
}
@Override
public void changedUpdate(final DocumentEvent event) {
update();
}
private synchronized void update() {
errorLabel.setText( " " );
if (identiconJob != null)
identiconJob.cancel( true );
identiconJob = Res.job( this::updateIdenticon, 100 + random.nextInt( 100 ), TimeUnit.MILLISECONDS );
}
private void updateIdenticon() {
char[] masterPassword = masterPasswordField.getPassword();
MPIdenticon identicon = ((masterPassword != null) && (masterPassword.length > 0))?
new MPIdenticon( user.getFullName(), masterPassword ): null;
Res.ui( () -> {
if (identicon != null) {
identiconLabel.setForeground(
Res.colors().fromIdenticonColor( identicon.getColor(), Res.Colors.BackgroundMode.LIGHT ) );
identiconLabel.setText( identicon.getText() );
} else {
identiconLabel.setForeground( null );
identiconLabel.setText( " " );
}
} );
}
}
private final class AuthenticatedUserPanel extends JPanel implements KeyListener, MPUser.Listener {
private final JButton userButton = Components.button( Res.icons().user(), event -> showUserPreferences(),
"Show user preferences." );
private final JButton logoutButton = Components.button( Res.icons().lock(), event -> logoutUser(),
"Sign out and lock user." );
private final JButton settingsButton = Components.button( Res.icons().settings(), event -> showSiteSettings(),
"Show site settings." );
private final JButton questionsButton = Components.button( Res.icons().question(), event -> showSiteQuestions(),
"Show site recovery questions." );
private final JButton editButton = Components.button( Res.icons().edit(), event -> showSiteValues(),
"Set/save personal password/login." );
private final JButton keyButton = Components.button( Res.icons().key(), event -> showSiteKeys(),
"Cryptographic site keys." );
private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteSite(),
"Delete the site from the user." );
@Nonnull
private final MPUser<?> user;
private final JLabel passwordLabel;
private final JLabel passwordField;
private final JLabel answerLabel;
private final JLabel answerField;
private final JLabel queryLabel;
private final JTextField queryField;
private final CollectionListModel<MPSite<?>> sitesModel;
private final JList<MPSite<?>> sitesList;
private Future<?> updateSitesJob;
private AuthenticatedUserPanel(@Nonnull final MPUser<?> user) {
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
this.user = user;
userToolbar.add( addButton );
userToolbar.add( userButton );
userToolbar.add( logoutButton );
userToolbar.add( Box.createGlue() );
userToolbar.add( helpButton );
siteToolbar.add( settingsButton );
siteToolbar.add( questionsButton );
siteToolbar.add( editButton );
siteToolbar.add( keyButton );
siteToolbar.add( deleteButton );
settingsButton.setEnabled( false );
questionsButton.setEnabled( false );
editButton.setEnabled( false );
keyButton.setEnabled( false );
deleteButton.setEnabled( false );
answerLabel = Components.label( "Answer:" );
answerField = Components.heading( SwingConstants.CENTER );
answerField.setForeground( Res.colors().highlightFg() );
answerField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
add( passwordLabel = Components.label( SwingConstants.CENTER ) );
add( passwordField = Components.heading( SwingConstants.CENTER ) );
passwordField.setForeground( Res.colors().highlightFg() );
passwordField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
add( Box.createGlue() );
add( Components.strut() );
add( queryLabel = Components.label() );
queryLabel.setText( strf( "%s's password for:", user.getFullName() ) );
add( queryField = Components.textField( null, this::updateSites ) );
queryField.putClientProperty( "JTextField.variant", "search" );
queryField.addActionListener( this::useSite );
queryField.getInputMap().put( copyLoginKeyStroke, JTextField.notifyAction );
queryField.addKeyListener( this );
queryField.requestFocusInWindow();
add( Components.strut() );
add( Components.scrollPane( sitesList = Components.list(
sitesModel = new CollectionListModel<MPSite<?>>().selection( this::showSiteResult ),
this::getSiteDescription ) ) );
add( Components.strut() );
add( Components.label( strf(
"Press %s to copy password, %s+%s to copy login name.",
KeyEvent.getKeyText( KeyEvent.VK_ENTER ),
InputEvent.getModifiersExText( copyLoginKeyStroke.getModifiers() ),
KeyEvent.getKeyText( copyLoginKeyStroke.getKeyCode() ) ) ) );
addHierarchyListener( e -> {
if (null != SwingUtilities.windowForComponent( this ))
user.addListener( this );
else
user.removeListener( this );
} );
}
public void showUserPreferences() {
ImmutableList.Builder<Component> components = ImmutableList.builder();
MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null;
if (fileUser != null)
components.add( Components.label( "Default Password Type:" ),
Components.comboBox( MPResultType.values(), MPResultType::getLongName,
fileUser.getDefaultType(), fileUser::setDefaultType ),
Components.strut() );
components.add( Components.label( "Default Algorithm:" ),
Components.comboBox( MPAlgorithm.Version.values(), MPAlgorithm.Version::name,
user.getAlgorithm().version(),
version -> user.setAlgorithm( version.getAlgorithm() ) ) );
Components.showDialog( this, user.getFullName(), new JOptionPane( Components.panel(
BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) );
}
public void logoutUser() {
user.invalidate();
}
public void showSiteSettings() {
MPSite<?> site = sitesModel.getSelectedItem();
if (site == null)
return;
ImmutableList.Builder<Component> components = ImmutableList.builder();
components.add( Components.label( "Algorithm:" ),
Components.comboBox( MPAlgorithm.Version.values(), MPAlgorithm.Version::name,
site.getAlgorithm().version(),
version -> site.setAlgorithm( version.getAlgorithm() ) ) );
components.add( Components.label( "Counter:" ),
Components.spinner( new UnsignedIntegerModel( site.getCounter(), UnsignedInteger.ONE )
.selection( site::setCounter ) ),
Components.strut() );
components.add( Components.label( "Password Type:" ),
Components.comboBox( MPResultType.values(), type -> getTypeDescription(
type, user.getDefaultType(), user.getAlgorithm().mpw_default_result_type() ),
site.getResultType(), site::setResultType ),
Components.strut() );
components.add( Components.label( "Login Type:" ),
Components.comboBox( MPResultType.values(), type -> getTypeDescription(
type, user.getAlgorithm().mpw_default_login_type() ),
site.getLoginType(), site::setLoginType ),
Components.strut() );
MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null;
if (fileSite != null)
components.add( Components.label( "URL:" ),
Components.textField( fileSite.getUrl(), fileSite::setUrl ),
Components.strut() );
Components.showDialog( this, strf( "Settings for %s", site.getSiteName() ), new JOptionPane( Components.panel(
BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) );
}
private String getTypeDescription(final MPResultType type, final MPResultType... defaults) {
boolean isDefault = false;
for (final MPResultType d : defaults)
if (isDefault = type == d)
break;
return strf( "<html>%s%s%s, %s", isDefault? "<b>": "", type.getLongName(), isDefault? "</b>": "", type.getDescription() );
}
public void showSiteQuestions() {
MPSite<?> site = sitesModel.getSelectedItem();
if (site == null)
return;
CollectionListModel<MPQuestion> questionsModel = new CollectionListModel<MPQuestion>().selection( this::showQuestionResult );
JList<MPQuestion> questionsList = Components.list(
questionsModel, question -> Strings.isNullOrEmpty( question.getKeyword() )? "<site>": question.getKeyword() );
JTextField queryField = Components.textField( null, query -> Res.job( () -> {
Collection<MPQuestion> questions = new LinkedList<>( site.findQuestions( query ) );
if (questions.stream().noneMatch( question -> question.getKeyword().equalsIgnoreCase( query ) ))
questions.add( new MPNewQuestion( site, Utilities.ifNotNullElse( query, "" ) ) );
Res.ui( () -> questionsModel.set( questions ) );
} ) );
queryField.putClientProperty( "JTextField.variant", "search" );
queryField.addActionListener( event -> useQuestion( questionsModel.getSelectedItem() ) );
queryField.addKeyListener( new KeyAdapter() {
@Override
public void keyPressed(final KeyEvent event) {
if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN))
questionsList.dispatchEvent( event );
}
@Override
public void keyReleased(final KeyEvent event) {
if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN))
questionsList.dispatchEvent( event );
}
} );
Components.showDialog( this, strf( "Recovery answers for %s", site.getSiteName() ), new JOptionPane( Components.panel(
BoxLayout.PAGE_AXIS,
Components.label( "Security Question Keyword:" ), queryField,
Components.strut(),
answerLabel, answerField,
Components.strut(),
Components.scrollPane( questionsList ) ) ) {
@Override
public void selectInitialValue() {
queryField.requestFocusInWindow();
}
} );
}
public void showSiteValues() {
MPSite<?> site = sitesModel.getSelectedItem();
if (site == null)
return;
try {
JTextField passwordField = Components.textField( site.getResult(), null );
JTextField loginField = Components.textField( site.getLogin(), null );
passwordField.setEditable( site.getResultType().getTypeClass() == MPResultTypeClass.Stateful );
loginField.setEditable( site.getLoginType().getTypeClass() == MPResultTypeClass.Stateful );
if (JOptionPane.OK_OPTION == Components.showDialog( this, site.getSiteName(), new JOptionPane(
Components.panel(
BoxLayout.PAGE_AXIS,
Components.label( strf( "<html>Site Login (currently set to: <b>%s</b>):",
getTypeDescription( site.getLoginType() ) ) ),
loginField,
Components.strut(),
Components.label( strf( "<html>Site Password (currently set to: <b>%s</b>):",
getTypeDescription( site.getResultType() ) ) ),
passwordField,
Components.strut(),
Components.label( "<html>To save a personal value in these fields,\n" +
"change the type to <b>Saved</b> in the site's settings." ) ),
JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION ) {
@Override
public void selectInitialValue() {
passwordField.requestFocusInWindow();
}
} )) {
if (site instanceof MPFileSite) {
MPFileSite fileSite = (MPFileSite) site;
if (site.getResultType().getTypeClass() == MPResultTypeClass.Stateful)
fileSite.setSitePassword( site.getResultType(), passwordField.getText() );
if (site.getLoginType().getTypeClass() == MPResultTypeClass.Stateful)
fileSite.setLoginName( site.getLoginType(), loginField.getText() );
}
}
}
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
logger.err( e, "While computing site edit results." );
}
}
public void showSiteKeys() {
MPSite<?> site = sitesModel.getSelectedItem();
if (site == null)
return;
JTextArea resultField = Components.textArea();
resultField.setEnabled( false );
CollectionListModel<MPKeyPurpose> purposeModel = new CollectionListModel<>( MPKeyPurpose.values() );
DocumentModel contextModel = new DocumentModel( new PlainDocument() );
UnsignedIntegerModel counterModel = new UnsignedIntegerModel( UnsignedInteger.ONE );
CollectionListModel<MPResultType> typeModel = new CollectionListModel<>( MPResultType.values() );
DocumentModel stateModel = new DocumentModel( new PlainDocument() );
Runnable trigger = () -> Res.job( () -> {
try {
MPKeyPurpose purpose = purposeModel.getSelectedItem();
MPResultType type = typeModel.getSelectedItem();
String result = ((purpose == null) || (type == null))? null:
site.getResult( purpose, contextModel.getText(), counterModel.getNumber(), type, stateModel.getText() );
Res.ui( () -> resultField.setText( result ) );
}
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
logger.err( e, "While computing site edit results." );
}
} );
purposeModel.selection( MPKeyPurpose.Authentication, p -> trigger.run() );
contextModel.selection( c -> trigger.run() );
counterModel.selection( c -> trigger.run() );
typeModel.selection( MPResultType.DeriveKey, t -> {
switch (t) {
case DeriveKey:
stateModel.setText( Integer.toString( site.getAlgorithm().mpw_keySize_min() ) );
break;
default:
stateModel.setText( null );
}
trigger.run();
} );
stateModel.selection( c -> trigger.run() );
if (JOptionPane.OK_OPTION == Components.showDialog( this, site.getSiteName(), new JOptionPane( Components.panel(
BoxLayout.PAGE_AXIS,
Components.heading( "Key Calculator" ),
Components.label( "Purpose:" ),
Components.comboBox( purposeModel, MPKeyPurpose::getShortName ),
Components.strut(),
Components.label( "Context:" ),
Components.textField( contextModel.getDocument() ),
Components.label( "Counter:" ),
Components.spinner( counterModel ),
Components.label( "Type:" ),
Components.comboBox( typeModel, this::getTypeDescription ),
Components.label( "State:" ),
Components.scrollPane( Components.textField( stateModel.getDocument() ) ),
Components.strut(),
resultField ) ) {
{
setOptions( new Object[]{ "Copy", "Cancel" } );
setInitialValue( getOptions()[0] );
}
} ))
copyResult( resultField.getText() );
}
public void deleteSite() {
MPSite<?> site = sitesModel.getSelectedItem();
if (site == null)
return;
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
this, strf( "<html>Forget the site <strong>%s</strong>?</html>", site.getSiteName() ),
"Delete Site", JOptionPane.YES_NO_OPTION ))
user.deleteSite( site );
}
private String getSiteDescription(@Nonnull final MPSite<?> site) {
if (site instanceof MPNewSite)
return strf( "<html><strong>%s</strong> &lt;Add new site&gt;</html>", queryField.getText() );
ImmutableList.Builder<Object> parameters = ImmutableList.builder();
try {
MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null;
if (fileSite != null)
parameters.add( Res.format( fileSite.getLastUsed() ) );
parameters.add( site.getAlgorithm().version() );
parameters.add( strf( "#%d", site.getCounter().longValue() ) );
parameters.add( strf( "<em>%s</em>", site.getLogin() ) );
if ((fileSite != null) && (fileSite.getUrl() != null))
parameters.add( fileSite.getUrl() );
}
catch (final MPAlgorithmException | MPKeyUnavailableException e) {
logger.err( e, "While generating site description." );
parameters.add( e.getLocalizedMessage() );
}
return strf( "<html><strong>%s</strong> (%s)</html>", site.getSiteName(),
Joiner.on( " - " ).skipNulls().join( parameters.build() ) );
}
private void useSite(final ActionEvent event) {
MPSite<?> site = sitesModel.getSelectedItem();
if (site instanceof MPNewSite) {
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
this, strf( "<html>Remember the site <strong>%s</strong>?</html>", site.getSiteName() ),
"New Site", JOptionPane.YES_NO_OPTION )) {
sitesModel.setSelectedItem( user.addSite( site.getSiteName() ) );
useSite( event );
}
return;
}
boolean loginResult = (copyLoginKeyStroke.getModifiers() & event.getModifiers()) != 0;
showSiteResult( site, loginResult, result -> {
if (result == null)
return;
if (site instanceof MPFileSite)
((MPFileSite) site).use();
copyResult( result );
} );
}
private void showSiteResult(@Nullable final MPSite<?> site) {
showSiteResult( site, false, result -> {
} );
}
private void showSiteResult(@Nullable final MPSite<?> site, final boolean loginResult, final Consumer<String> resultCallback) {
Res.job( () -> {
try {
if (site != null)
return loginResult? site.getLogin(): site.getResult();
}
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
logger.err( e, "While resolving password for: %s", site );
}
return null;
}, resultCallback.andThen( result -> Res.ui( () -> {
passwordLabel.setText( ((result != null) && (site != null))? strf( "Your password for %s:", site.getSiteName() ): " " );
passwordField.setText( (result != null)? result: " " );
settingsButton.setEnabled( result != null );
questionsButton.setEnabled( result != null );
editButton.setEnabled( result != null );
keyButton.setEnabled( result != null );
deleteButton.setEnabled( result != null );
} ) ) );
}
private void useQuestion(@Nullable final MPQuestion question) {
if (question instanceof MPNewQuestion) {
if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
this,
strf( "<html>Remember the security question with keyword <strong>%s</strong>?</html>",
Strings.isNullOrEmpty( question.getKeyword() )? "<empty>": question.getKeyword() ),
"New Question", JOptionPane.YES_NO_OPTION )) {
useQuestion( question.getSite().addQuestion( question.getKeyword() ) );
}
return;
}
showQuestionResult( question, result -> {
if (result == null)
return;
if (question instanceof MPFileQuestion)
((MPFileQuestion) question).use();
copyResult( result );
} );
}
private void showQuestionResult(@Nullable final MPQuestion question) {
showQuestionResult( question, answer -> {
} );
}
private void showQuestionResult(@Nullable final MPQuestion question, final Consumer<String> resultCallback) {
Res.job( () -> {
try {
if (question != null)
return question.getAnswer();
}
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
logger.err( e, "While resolving answer for: %s", question );
}
return null;
}, resultCallback.andThen( answer -> Res.ui( () -> {
if ((answer == null) || (question == null))
answerLabel.setText( " " );
else
answerLabel.setText(
Strings.isNullOrEmpty( question.getKeyword() )?
strf( "<html>Answer for site <b>%s</b>:", question.getSite().getSiteName() ):
strf( "<html>Answer for site <b>%s</b>, of question with keyword <b>%s</b>:",
question.getSite().getSiteName(), question.getKeyword() ) );
answerField.setText( (answer != null)? answer: " " );
} ) ) );
}
private void copyResult(final String result) {
Transferable clipboardContents = new StringSelection( result );
Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null );
Res.ui( () -> {
Window answerDialog = SwingUtilities.windowForComponent( answerField );
if (answerDialog instanceof Dialog)
answerDialog.setVisible( false );
Window window = SwingUtilities.windowForComponent( UserContentPanel.this );
if (window instanceof Frame)
((Frame) window).setExtendedState( Frame.ICONIFIED );
} );
}
@Override
public void keyTyped(final KeyEvent event) {
}
@Override
public void keyPressed(final KeyEvent event) {
if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN))
sitesList.dispatchEvent( event );
}
@Override
public void keyReleased(final KeyEvent event) {
if ((event.getKeyCode() == KeyEvent.VK_UP) || (event.getKeyCode() == KeyEvent.VK_DOWN))
sitesList.dispatchEvent( event );
}
private synchronized void updateSites(@Nullable final String query) {
if (updateSitesJob != null)
updateSitesJob.cancel( true );
updateSitesJob = Res.job( () -> {
Collection<MPSite<?>> sites = new LinkedList<>( user.findSites( query ) );
if (!Strings.isNullOrEmpty( query ))
if (sites.stream().noneMatch( site -> site.getSiteName().equalsIgnoreCase( query ) ))
sites.add( new MPNewSite( user, query ) );
Res.ui( () -> sitesModel.set( sites ) );
} );
}
@Override
public void onUserUpdated(final MPUser<?> user) {
updateSites( queryField.getText() );
showSiteResult( sitesModel.getSelectedItem() );
}
@Override
public void onUserAuthenticated(final MPUser<?> user) {
}
@Override
public void onUserInvalidated(final MPUser<?> user) {
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

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.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

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

View File

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

View File

@@ -18,6 +18,7 @@
package com.lyndir.masterpassword.model;
import com.google.common.collect.ImmutableCollection;
import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.*;
import java.util.Collection;
@@ -33,9 +34,7 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
// - Meta
@Nonnull
String getName();
void setName(String name);
String getSiteName();
// - Algorithm
@@ -59,10 +58,44 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
void setLoginType(@Nullable MPResultType loginType);
@Nonnull
@Nullable
default String getResult()
throws MPKeyUnavailableException, MPAlgorithmException {
return getResult( MPKeyPurpose.Authentication );
}
@Nullable
default String getResult(final MPKeyPurpose keyPurpose)
throws MPKeyUnavailableException, MPAlgorithmException {
return getResult( keyPurpose, null );
}
@Nullable
default String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext)
throws MPKeyUnavailableException, MPAlgorithmException {
return getResult( keyPurpose, keyContext, null );
}
@Nullable
String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state)
throws MPKeyUnavailableException, MPAlgorithmException;
@Nullable
String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext,
@Nullable UnsignedInteger counter, MPResultType type, @Nullable String state)
throws MPKeyUnavailableException, MPAlgorithmException;
@Nonnull
String getState(MPKeyPurpose keyPurpose, @Nullable String keyContext,
@Nullable UnsignedInteger counter, MPResultType type, String state)
throws MPKeyUnavailableException, MPAlgorithmException;
@Nonnull
default String getLogin()
throws MPKeyUnavailableException, MPAlgorithmException {
return getLogin( null );
}
@Nonnull
String getLogin(@Nullable String state)
throws MPKeyUnavailableException, MPAlgorithmException;
@@ -72,10 +105,17 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
@Nonnull
MPUser<?> getUser();
void addQuestion(Q question);
@Nonnull
Q addQuestion(String keyword);
void deleteQuestion(Q question);
@Nonnull
Q addQuestion(Q question);
boolean deleteQuestion(Q question);
@Nonnull
Collection<Q> getQuestions();
@Nonnull
ImmutableCollection<Q> findQuestions(@Nullable String query);
}

View File

@@ -18,6 +18,7 @@
package com.lyndir.masterpassword.model;
import com.google.common.collect.ImmutableCollection;
import com.lyndir.masterpassword.*;
import java.util.Collection;
import javax.annotation.Nonnull;
@@ -45,6 +46,11 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
void setAlgorithm(MPAlgorithm algorithm);
@Nullable
default MPResultType getDefaultType() {
return null;
}
@Nullable
byte[] getKeyID();
@@ -54,12 +60,11 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
/**
* Performs an authentication attempt against the keyID for this user.
*
* Note: If a keyID is not set, authentication will always succeed and the keyID will be set to match the given master password.
*
* @param masterPassword The password to authenticate with.
* You cannot re-use this array after passing it in, authentication will destroy its contents.
*
* @throws MPIncorrectMasterPasswordException If authentication fails due to the given master password not matching the user's keyID.
* @apiNote If a keyID is not set, authentication will always succeed and the keyID will be set to match the given master password.
* <b>This method destroys the contents of the {@code masterPassword} array.</b>
*/
void authenticate(char[] masterPassword)
throws MPIncorrectMasterPasswordException, MPAlgorithmException;
@@ -67,15 +72,26 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
/**
* Performs an authentication attempt against the keyID for this user.
*
* Note: If a keyID is not set, authentication will always succeed and the keyID will be set to match the given key.
*
* @param masterKey The master key to authenticate with.
*
* @throws MPIncorrectMasterPasswordException If authentication fails due to the given master password not matching the user's keyID.
* @apiNote If a keyID is not set, authentication will always succeed and the keyID will be set to match the given key.
*/
void authenticate(MPMasterKey masterKey)
throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException;
/**
* Clear all authentication tokens and secrets from memory, effectively logging the user out.
*/
void invalidate();
/**
* Wipe the key ID, allowing the user to {@link #authenticate(char[])} with any master password.
*
* Note: Authenticating with a different master password will cause all of the user's results to change.
*/
void reset();
boolean isMasterKeyAvailable();
@Nonnull
@@ -84,13 +100,30 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
// - Relations
void addSite(S site);
@Nonnull
S addSite(String siteName);
void deleteSite(S site);
@Nonnull
S addSite(S site);
boolean deleteSite(MPSite<?> site);
@Nonnull
Collection<S> getSites();
@Nonnull
Collection<S> findSites(String query);
ImmutableCollection<S> findSites(@Nullable String query);
void addListener(Listener listener);
void removeListener(Listener listener);
interface Listener {
void onUserUpdated(MPUser<?> user);
void onUserAuthenticated(MPUser<?> user);
void onUserInvalidated(MPUser<?> user);
}
}

View File

@@ -1,53 +0,0 @@
//==============================================================================
// This file is part of Master Password.
// Copyright (c) 2011-2017, Maarten Billemont.
//
// Master Password is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Master Password is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You can find a copy of the GNU General Public License in the
// LICENSE file. Alternatively, see <http://www.gnu.org/licenses/>.
//==============================================================================
package com.lyndir.masterpassword.model;
import com.google.common.collect.*;
import java.util.Collection;
import java.util.Map;
/**
* @author lhunath, 14-12-05
*/
public abstract class MPUserManager<U extends MPUser<?>> {
private final Map<String, U> usersByName = Maps.newHashMap();
protected MPUserManager(final Iterable<U> users) {
for (final U user : users)
usersByName.put( user.getFullName(), user );
}
public Collection<U> getUsers() {
return ImmutableSortedSet.copyOf( usersByName.values() );
}
public U getUserNamed(final String fullName) {
return usersByName.get( fullName );
}
public void addUser(final U user) {
usersByName.put( user.getFullName(), user );
}
public void deleteUser(final U user) {
usersByName.remove( user.getFullName() );
}
}

View File

@@ -11,42 +11,50 @@ public class Changeable {
private static final ExecutorService changeExecutor = Executors.newSingleThreadExecutor();
private boolean changed;
private boolean batchingChanges;
private final Object mutex = new Object();
private Grouping grouping = Grouping.APPLY;
private boolean changed;
void setChanged() {
synchronized (changeExecutor) {
synchronized (mutex) {
if (changed)
return;
changed = true;
if (batchingChanges)
if (grouping != Grouping.IGNORE)
changed = true;
if (grouping != Grouping.APPLY)
return;
changeExecutor.submit( () -> {
synchronized (changeExecutor) {
if (batchingChanges)
return;
changed = false;
}
onChanged();
} );
}
changeExecutor.submit( () -> {
synchronized (changeExecutor) {
if (grouping != Grouping.APPLY)
return;
changed = false;
}
onChanged();
} );
}
protected void onChanged() {
}
public void beginChanges() {
synchronized (changeExecutor) {
batchingChanges = true;
synchronized (mutex) {
grouping = Grouping.BATCH;
}
}
public void ignoreChanges() {
synchronized (mutex) {
grouping = Grouping.IGNORE;
}
}
public boolean endChanges() {
synchronized (changeExecutor) {
batchingChanges = false;
synchronized (mutex) {
grouping = Grouping.APPLY;
if (changed) {
this.changed = false;
@@ -56,4 +64,8 @@ public class Changeable {
return false;
}
}
private enum Grouping {
APPLY, BATCH, IGNORE
}
}

View File

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

View File

@@ -21,53 +21,52 @@ package com.lyndir.masterpassword.model.impl;
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
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.ImmutableSortedSet;
import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPQuestion;
import com.lyndir.masterpassword.model.MPSite;
import com.lyndir.masterpassword.model.*;
import java.util.*;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.jetbrains.annotations.NotNull;
/**
* @author lhunath, 14-12-16
*/
public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable implements MPSite<Q> {
public abstract class MPBasicSite<U extends MPUser<?>, Q extends MPQuestion> extends Changeable
implements MPSite<Q> {
private final U user;
private final String siteName;
private final Collection<Q> questions = new LinkedHashSet<>();
private String name;
private MPAlgorithm algorithm;
private UnsignedInteger counter;
private MPResultType resultType;
private MPResultType loginType;
private final Collection<Q> questions = new LinkedHashSet<>();
protected MPBasicSite(final String name, final MPAlgorithm algorithm) {
this( name, algorithm, null, null, null );
protected MPBasicSite(final U user, final String siteName) {
this( user, siteName, null, null, null, null );
}
protected MPBasicSite(final String name, final MPAlgorithm algorithm, @Nullable final UnsignedInteger counter,
protected MPBasicSite(final U user, final String siteName,
@Nullable final MPAlgorithm algorithm, @Nullable final UnsignedInteger counter,
@Nullable final MPResultType resultType, @Nullable final MPResultType loginType) {
this.name = name;
this.algorithm = algorithm;
this.counter = (counter == null)? algorithm.mpw_default_counter(): counter;
this.resultType = (resultType == null)? algorithm.mpw_default_result_type(): resultType;
this.loginType = (loginType == null)? algorithm.mpw_default_login_type(): loginType;
this.user = user;
this.siteName = siteName;
this.algorithm = (algorithm != null)? algorithm: this.user.getAlgorithm();
this.counter = (counter != null)? counter: this.algorithm.mpw_default_counter();
this.resultType = (resultType != null)? resultType:
ifNotNullElse( this.user.getDefaultType(), this.algorithm.mpw_default_result_type() );
this.loginType = (loginType != null)? loginType: this.algorithm.mpw_default_login_type();
}
@Nonnull
@Override
public String getName() {
return name;
}
@Override
public void setName(final String name) {
this.name = name;
setChanged();
public String getSiteName() {
return siteName;
}
@Nonnull
@@ -78,8 +77,10 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
@Override
public void setAlgorithm(final MPAlgorithm algorithm) {
this.algorithm = algorithm;
if (Objects.equals( this.algorithm, algorithm ))
return;
this.algorithm = algorithm;
setChanged();
}
@@ -91,8 +92,10 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
@Override
public void setCounter(final UnsignedInteger counter) {
this.counter = counter;
if (Objects.equals( this.counter, counter ))
return;
this.counter = counter;
setChanged();
}
@@ -104,8 +107,10 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
@Override
public void setResultType(final MPResultType resultType) {
this.resultType = resultType;
if (this.resultType == resultType)
return;
this.resultType = resultType;
setChanged();
}
@@ -117,12 +122,14 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
@Override
public void setLoginType(@Nullable final MPResultType loginType) {
this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() );
if (this.loginType == loginType)
return;
this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() );
setChanged();
}
@Nonnull
@Nullable
@Override
public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext, @Nullable final String state)
throws MPKeyUnavailableException, MPAlgorithmException {
@@ -130,21 +137,25 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
return getResult( keyPurpose, keyContext, getCounter(), getResultType(), state );
}
protected String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
@Nullable final UnsignedInteger counter, final MPResultType type, @Nullable final String state)
@Nullable
@Override
public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
@Nullable final UnsignedInteger counter, final MPResultType type, @Nullable final String state)
throws MPKeyUnavailableException, MPAlgorithmException {
return getUser().getMasterKey().siteResult(
getName(), getAlgorithm(), ifNotNullElse( counter, getAlgorithm().mpw_default_counter() ),
getSiteName(), getAlgorithm(), ifNotNullElse( counter, getAlgorithm().mpw_default_counter() ),
keyPurpose, keyContext, type, state );
}
protected String getState(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
@Nullable final UnsignedInteger counter, final MPResultType type, final String state)
@Nonnull
@Override
public String getState(final MPKeyPurpose keyPurpose, @Nullable final String keyContext,
@Nullable final UnsignedInteger counter, final MPResultType type, final String state)
throws MPKeyUnavailableException, MPAlgorithmException {
return getUser().getMasterKey().siteState(
getName(), getAlgorithm(), ifNotNullElse( counter, getAlgorithm().mpw_default_counter() ),
getSiteName(), getAlgorithm(), ifNotNullElse( counter, getAlgorithm().mpw_default_counter() ),
keyPurpose, keyContext, type, state );
}
@@ -156,18 +167,22 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
return getResult( MPKeyPurpose.Identification, null, null, getLoginType(), state );
}
@Nonnull
@Override
public void addQuestion(final Q question) {
public Q addQuestion(final Q question) {
questions.add( question );
setChanged();
return question;
}
@Override
public void deleteQuestion(final Q question) {
questions.remove( question );
public boolean deleteQuestion(final Q question) {
if (!questions.remove( question ))
return false;
setChanged();
return true;
}
@Nonnull
@@ -178,32 +193,46 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
@Nonnull
@Override
public abstract MPBasicUser<?> getUser();
public ImmutableCollection<Q> findQuestions(@Nullable final String query) {
ImmutableSortedSet.Builder<Q> results = ImmutableSortedSet.naturalOrder();
for (final Q question : getQuestions())
if (Strings.isNullOrEmpty( query ) || question.getKeyword().startsWith( query ))
results.add( question );
return results.build();
}
@Nonnull
@Override
public U getUser() {
return user;
}
@Override
protected void onChanged() {
super.onChanged();
getUser().setChanged();
if (user instanceof Changeable)
((Changeable) user).setChanged();
}
@Override
public int hashCode() {
return Objects.hashCode( getName() );
return Objects.hashCode( getSiteName() );
}
@Override
public boolean equals(final Object obj) {
return (this == obj) || ((obj instanceof MPSite) && Objects.equals( getName(), ((MPSite<?>) obj).getName() ));
return obj == this;
}
@Override
public int compareTo(@NotNull final MPSite<?> o) {
return getName().compareTo( o.getName() );
public int compareTo(@Nonnull final MPSite<?> o) {
return getSiteName().compareTo( o.getSiteName() );
}
@Override
public String toString() {
return strf( "{%s: %s}", getClass().getSimpleName(), getName() );
return strf( "{%s: %s}", getClass().getSimpleName(), getSiteName() );
}
}

View File

@@ -20,13 +20,15 @@ package com.lyndir.masterpassword.model.impl;
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.ImmutableSortedSet;
import com.lyndir.lhunath.opal.system.CodeUtils;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
import com.lyndir.masterpassword.model.MPUser;
import com.lyndir.masterpassword.model.*;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -34,9 +36,11 @@ import javax.annotation.Nullable;
/**
* @author lhunath, 2014-06-08
*/
public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable implements MPUser<S> {
public abstract class MPBasicUser<S extends MPBasicSite<?, ?>> extends Changeable implements MPUser<S> {
protected final Logger logger = Logger.get( getClass() );
private static final Logger logger = Logger.get( MPBasicUser.class );
private final Set<Listener> listeners = new CopyOnWriteArraySet<>();
private int avatar;
private final String fullName;
@@ -44,7 +48,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
@Nullable
protected MPMasterKey masterKey;
private final Collection<S> sites = new LinkedHashSet<>();
private final Map<String, S> sites = new LinkedHashMap<>();
protected MPBasicUser(final String fullName, final MPAlgorithm algorithm) {
this( 0, fullName, algorithm );
@@ -63,8 +67,10 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
@Override
public void setAvatar(final int avatar) {
this.avatar = avatar;
if (Objects.equals( this.avatar, avatar ))
return;
this.avatar = avatar;
setChanged();
}
@@ -82,8 +88,10 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
@Override
public void setAlgorithm(final MPAlgorithm algorithm) {
this.algorithm = algorithm;
if (Objects.equals( this.algorithm, algorithm ))
return;
this.algorithm = algorithm;
setChanged();
}
@@ -91,12 +99,14 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
@Override
public byte[] getKeyID() {
try {
return getMasterKey().getKeyID( getAlgorithm() );
if (isMasterKeyAvailable())
return getMasterKey().getKeyID( getAlgorithm() );
}
catch (final MPException e) {
logger.wrn( e, "While deriving key ID for user: %s", this );
return null;
}
return null;
}
@Nullable
@@ -128,54 +138,96 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
throw new MPIncorrectMasterPasswordException( this );
this.masterKey = masterKey;
for (final Listener listener : listeners)
listener.onUserAuthenticated( this );
}
@Override
public void invalidate() {
if (masterKey == null)
return;
masterKey.invalidate();
masterKey = null;
for (final Listener listener : listeners)
listener.onUserInvalidated( this );
}
@Override
public void reset() {
invalidate();
}
@Override
public boolean isMasterKeyAvailable() {
return masterKey != null;
return (masterKey != null) && masterKey.isValid();
}
@Nonnull
@Override
public MPMasterKey getMasterKey()
throws MPKeyUnavailableException {
if (masterKey == null)
if ((masterKey == null) || !masterKey.isValid())
throw new MPKeyUnavailableException( "Master key was not yet set for: " + this );
return masterKey;
}
@Nonnull
@Override
public void addSite(final S site) {
sites.add( site );
public S addSite(final S site) {
sites.put( site.getSiteName(), site );
setChanged();
return site;
}
@Override
public void deleteSite(final S site) {
sites.remove( site );
public boolean deleteSite(final MPSite<?> site) {
if (!sites.values().remove( site ))
return false;
setChanged();
return true;
}
@Nonnull
@Override
public Collection<S> getSites() {
return Collections.unmodifiableCollection( sites );
return Collections.unmodifiableCollection( sites.values() );
}
@Nonnull
@Override
public Collection<S> findSites(final String query) {
public ImmutableCollection<S> findSites(@Nullable final String query) {
ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder();
for (final S site : getSites())
if (site.getName().startsWith( query ))
if (Strings.isNullOrEmpty( query ) || site.getSiteName().startsWith( query ))
results.add( site );
return results.build();
}
@Override
public void addListener(final Listener listener) {
listeners.add( listener );
}
@Override
public void removeListener(final Listener listener) {
listeners.remove( listener );
}
@Override
protected void onChanged() {
super.onChanged();
for (final Listener listener : listeners)
listener.onUserUpdated( this );
}
@Override
public int hashCode() {
return Objects.hashCode( getFullName() );
@@ -183,11 +235,11 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
@Override
public boolean equals(final Object obj) {
return (this == obj) || ((obj instanceof MPUser) && Objects.equals( getFullName(), ((MPUser<?>) obj).getFullName() ));
return this == obj;
}
@Override
public int compareTo(final MPUser<?> o) {
public int compareTo(@Nonnull final MPUser<?> o) {
return getFullName().compareTo( o.getFullName() );
}

View File

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

View File

@@ -21,6 +21,7 @@ package com.lyndir.masterpassword.model.impl;
import com.google.common.primitives.UnsignedInteger;
import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPSite;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.joda.time.Instant;
@@ -31,9 +32,7 @@ import org.joda.time.ReadableInstant;
* @author lhunath, 14-12-05
*/
@SuppressWarnings("ComparableImplementedButEqualsNotOverridden")
public class MPFileSite extends MPBasicSite<MPFileQuestion> {
private final MPFileUser user;
public class MPFileSite extends MPBasicSite<MPFileUser, MPFileQuestion> {
@Nullable
private String url;
@@ -46,14 +45,8 @@ public class MPFileSite extends MPBasicSite<MPFileQuestion> {
private String loginState;
public MPFileSite(final MPFileUser user, final String name) {
this( user, name, null, null, null );
}
public MPFileSite(final MPFileUser user, final String name,
@Nullable final MPAlgorithm algorithm, @Nullable final UnsignedInteger counter,
@Nullable final MPResultType resultType) {
this( user, name, algorithm, counter, resultType, null,
null, null, null, 0, new Instant() );
this( user, name, null, null, null, null, null, null,
null, 0, new Instant() );
}
protected MPFileSite(final MPFileUser user, final String name,
@@ -61,10 +54,8 @@ public class MPFileSite extends MPBasicSite<MPFileQuestion> {
@Nullable final MPResultType resultType, @Nullable final String resultState,
@Nullable final MPResultType loginType, @Nullable final String loginState,
@Nullable final String url, final int uses, final ReadableInstant lastUsed) {
super( name, (algorithm == null)? user.getAlgorithm(): algorithm, counter,
(resultType == null)? user.getDefaultType(): resultType, loginType );
super( user, name, algorithm, counter, resultType, loginType );
this.user = user;
this.resultState = resultState;
this.loginState = loginState;
this.url = url;
@@ -77,9 +68,13 @@ public class MPFileSite extends MPBasicSite<MPFileQuestion> {
return url;
}
public void setUrl(@Nullable final String url) {
this.url = url;
public void setUrl(@Nullable String url) {
if ((url != null) && url.isEmpty())
url = null;
if (Objects.equals( this.url, url))
return;
this.url = url;
setChanged();
}
@@ -94,21 +89,20 @@ public class MPFileSite extends MPBasicSite<MPFileQuestion> {
public void use() {
uses++;
lastUsed = new Instant();
user.use();
}
public String getResult()
throws MPKeyUnavailableException, MPAlgorithmException {
return getResult( MPKeyPurpose.Authentication, null );
getUser().use();
setChanged();
}
@Nullable
@Override
public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext)
throws MPKeyUnavailableException, MPAlgorithmException {
return getResult( keyPurpose, keyContext, getResultState() );
}
@Nonnull
@Override
public String getLogin()
throws MPKeyUnavailableException, MPAlgorithmException {
@@ -153,13 +147,13 @@ public class MPFileSite extends MPBasicSite<MPFileQuestion> {
@Nonnull
@Override
public MPFileUser getUser() {
return user;
public MPFileQuestion addQuestion(final String keyword) {
return addQuestion( new MPFileQuestion( this, keyword, null, null ) );
}
@Override
public int compareTo(final MPSite<?> o) {
int comparison = (o instanceof MPFileSite)? -getLastUsed().compareTo( ((MPFileSite) o).getLastUsed() ): 0;
public int compareTo(@Nonnull final MPSite<?> o) {
int comparison = (o instanceof MPFileSite)? ((MPFileSite) o).getLastUsed().compareTo( getLastUsed() ): 0;
if (comparison != 0)
return comparison;

View File

@@ -18,9 +18,13 @@
package com.lyndir.masterpassword.model.impl;
import com.lyndir.lhunath.opal.system.logging.Logger;
import com.lyndir.masterpassword.*;
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
import com.lyndir.masterpassword.model.MPUser;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.joda.time.Instant;
@@ -33,34 +37,46 @@ import org.joda.time.ReadableInstant;
@SuppressWarnings("ComparableImplementedButEqualsNotOverridden")
public class MPFileUser extends MPBasicUser<MPFileSite> {
private static final Logger logger = Logger.get( MPFileUser.class );
@Nullable
private byte[] keyID;
private File path;
private MPMarshalFormat format;
private MPMarshaller.ContentMode contentMode;
private MPResultType defaultType;
private ReadableInstant lastUsed;
private boolean complete;
@Nullable
private MPJSONFile json;
public static MPFileUser load(final File file)
throws IOException, MPMarshalException {
for (final MPMarshalFormat format : MPMarshalFormat.values())
if (file.getName().endsWith( format.fileSuffix() ))
return format.unmarshaller().readUser( file );
public MPFileUser(final String fullName) {
this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm() );
return null;
}
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm) {
this( fullName, keyID, algorithm, 0, algorithm.mpw_default_result_type(), new Instant(),
MPMarshalFormat.DEFAULT, MPMarshaller.ContentMode.PROTECTED );
public MPFileUser(final String fullName, final File path) {
this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm(), path );
}
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final File path) {
this( fullName, keyID, algorithm, 0, null, new Instant(),
MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, path );
}
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm,
final int avatar, final MPResultType defaultType, final ReadableInstant lastUsed,
final MPMarshalFormat format, final MPMarshaller.ContentMode contentMode) {
final int avatar, @Nullable final MPResultType defaultType, final ReadableInstant lastUsed,
final MPMarshaller.ContentMode contentMode, final MPMarshalFormat format, final File path) {
super( avatar, fullName, algorithm );
this.keyID = (keyID == null)? null: keyID.clone();
this.defaultType = defaultType;
this.keyID = (keyID != null)? keyID.clone(): null;
this.defaultType = (defaultType != null)? defaultType: algorithm.mpw_default_result_type();
this.lastUsed = lastUsed;
this.path = path;
this.format = format;
this.contentMode = contentMode;
}
@@ -71,6 +87,10 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
return (keyID == null)? null: keyID.clone();
}
public void setPath(final File path) {
this.path = path;
}
@Override
public void setAlgorithm(final MPAlgorithm algorithm) {
if (!algorithm.equals( getAlgorithm() ) && (keyID != null)) {
@@ -96,8 +116,10 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
}
public void setFormat(final MPMarshalFormat format) {
this.format = format;
if (Objects.equals( this.format, format ))
return;
this.format = format;
setChanged();
}
@@ -106,18 +128,23 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
}
public void setContentMode(final MPMarshaller.ContentMode contentMode) {
this.contentMode = contentMode;
if (Objects.equals( this.contentMode, contentMode ))
return;
this.contentMode = contentMode;
setChanged();
}
@Override
public MPResultType getDefaultType() {
return defaultType;
}
public void setDefaultType(final MPResultType defaultType) {
this.defaultType = defaultType;
if (Objects.equals( this.defaultType, defaultType ))
return;
this.defaultType = defaultType;
setChanged();
}
@@ -127,17 +154,19 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
public void use() {
lastUsed = new Instant();
setChanged();
}
public void setJSON(final MPJSONFile json) {
this.json = json;
protected boolean isComplete() {
return complete;
}
@Nonnull
public MPJSONFile getJSON() {
return (json == null)? json = new MPJSONFile(): json;
protected void setComplete() {
complete = true;
}
public File getFile() {
return new File( path, getFullName() + getFormat().fileSuffix() );
}
@Override
@@ -145,33 +174,55 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
super.authenticate( masterKey );
try {
getFormat().unmarshaller().readSites( this );
}
catch (final IOException | MPMarshalException e) {
logger.err( e, "While reading sites on authentication." );
}
if (keyID == null) {
keyID = masterKey.getKeyID( getAlgorithm() );
setChanged();
}
}
public void save() {
try {
if (isComplete())
getFormat().marshaller().marshall( this );
}
catch (final MPKeyUnavailableException e) {
logger.wrn( e, "Cannot write out changes for unauthenticated user: %s.", this );
}
catch (final IOException | MPMarshalException | MPAlgorithmException e) {
logger.err( e, "Unable to write out changes for user: %s", this );
}
}
@Override
public void reset() {
keyID = null;
super.reset();
}
@Nonnull
@Override
public MPFileSite addSite(final String siteName) {
return addSite( new MPFileSite( this, siteName ) );
}
@Override
protected void onChanged() {
save();
super.onChanged();
try {
save();
}
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
logger.wrn( e, "Couldn't save change." );
}
}
void save()
throws MPKeyUnavailableException, MPAlgorithmException {
MPFileUserManager.get().save( this, getMasterKey() );
}
@Override
public int compareTo(final MPUser<?> o) {
int comparison = (o instanceof MPFileUser)? -getLastUsed().compareTo( ((MPFileUser) o).getLastUsed() ): 0;
public int compareTo(@Nonnull final MPUser<?> o) {
int comparison = (o instanceof MPFileUser)? ((MPFileUser) o).getLastUsed().compareTo( getLastUsed() ): 0;
if (comparison != 0)
return comparison;

Some files were not shown because too many files have changed in this diff Show More