Compare commits
41 Commits
2.7-java
...
2.7-java-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1264cad377 | ||
|
|
d185a0af14 | ||
|
|
4275a6cc61 | ||
|
|
c94ff429e8 | ||
|
|
00744cb264 | ||
|
|
7202fe6d1d | ||
|
|
63b4d9cd2e | ||
|
|
36a7c7f423 | ||
|
|
c2c4fb18bf | ||
|
|
3fc8acba70 | ||
|
|
f5c0c4d787 | ||
|
|
86775f1c75 | ||
|
|
2bb190f49a | ||
|
|
77c4a2af46 | ||
|
|
3da82d30b1 | ||
|
|
97532fdce6 | ||
|
|
fe63a2756a | ||
|
|
928b617ed0 | ||
|
|
18ecc41b39 | ||
|
|
a6e9e89ace | ||
|
|
0b7494ecbf | ||
|
|
8377c9c615 | ||
|
|
37a7cfa530 | ||
|
|
978b758079 | ||
|
|
38f09021b3 | ||
|
|
7455fba55e | ||
|
|
8cd9755616 | ||
|
|
46d301df94 | ||
|
|
e639137304 | ||
|
|
7c83a62f91 | ||
|
|
513840e2c4 | ||
|
|
8f7faa9e4e | ||
|
|
16cdcda94b | ||
|
|
400ebe59db | ||
|
|
476a4046e7 | ||
|
|
3403449ca2 | ||
|
|
596ace51ea | ||
|
|
80b5fcd785 | ||
|
|
a16bc9a318 | ||
|
|
462dd4e89b | ||
|
|
e5ff374a9c |
@@ -14,5 +14,5 @@ build_project:
|
|||||||
- "( xcodebuild -workspace platform-darwin/MasterPassword.xcworkspace -configuration 'Test' -scheme 'MasterPassword macOS' clean build )"
|
- "( xcodebuild -workspace platform-darwin/MasterPassword.xcworkspace -configuration 'Test' -scheme 'MasterPassword macOS' clean build )"
|
||||||
tags:
|
tags:
|
||||||
- brew
|
- brew
|
||||||
- java
|
- java_9
|
||||||
- xcode_9
|
- xcode_9
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
FROM alpine
|
FROM debian:buster-slim
|
||||||
|
|
||||||
# For i386
|
# For i386
|
||||||
#FROM i386/alpine
|
#FROM i386/debian:buster-slim
|
||||||
#ENTRYPOINT ["linux32", "--"]
|
#ENTRYPOINT ["linux32", "--"]
|
||||||
|
|
||||||
RUN apk add --no-cache git libtool automake autoconf make g++ bash openjdk8
|
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199
|
||||||
|
RUN mkdir -p /usr/share/man/man1
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install openjdk-10-jdk-headless git-core bash libtool automake autoconf make g++
|
||||||
RUN git clone --depth=3 $(: --shallow-submodules) --recurse-submodules https://gitlab.com/MasterPassword/MasterPassword.git /mpw
|
RUN git clone --depth=3 $(: --shallow-submodules) --recurse-submodules https://gitlab.com/MasterPassword/MasterPassword.git /mpw
|
||||||
RUN cd /mpw/gradle && ./gradlew -i clean build
|
RUN cd /mpw/gradle && ./gradlew -i clean build
|
||||||
|
|||||||
2
gradle/.idea/misc.xml
generated
@@ -24,7 +24,7 @@
|
|||||||
</value>
|
</value>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</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" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ allprojects {
|
|||||||
apply plugin: 'findbugs'
|
apply plugin: 'findbugs'
|
||||||
|
|
||||||
group = 'com.lyndir.masterpassword'
|
group = 'com.lyndir.masterpassword'
|
||||||
version = 'GIT-SNAPSHOT'
|
version = '2.7.3'
|
||||||
|
|
||||||
tasks.withType( JavaCompile ) {
|
tasks.withType( JavaCompile ) {
|
||||||
options.encoding = 'UTF-8'
|
options.encoding = 'UTF-8'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
org.gradle.daemon=true
|
org.gradle.daemon=true
|
||||||
org.gradle.configureondemand=true
|
org.gradle.configureondemand=true
|
||||||
org.gradle.jvmargs=-Xmx1536M
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.enableD8.desugaring=true
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
# target_prepare() { make -s distclean; }
|
# target_prepare() { make -s distclean; }
|
||||||
# target_configure() { _target_configure "$@" --enable-minimal; }
|
# target_configure() { _target_configure "$@" --enable-minimal; }
|
||||||
set -e
|
set -e
|
||||||
|
PATH+=:/usr/local/bin
|
||||||
|
|
||||||
# needs <binary> ...
|
# needs <binary> ...
|
||||||
#
|
#
|
||||||
@@ -31,8 +32,15 @@ set -e
|
|||||||
needs() { _needs "$@"; }
|
needs() { _needs "$@"; }
|
||||||
_needs() {
|
_needs() {
|
||||||
local failed=0
|
local failed=0
|
||||||
for tool; do
|
for spec; do
|
||||||
hash "$tool" || { echo >&2 "Missing: $tool. Please install this tool."; (( failed++ )); }
|
IFS=: read pkg tools <<< "$spec"
|
||||||
|
IFS=, read -a tools <<< "${tools:-$pkg}"
|
||||||
|
for tool in "${tools[@]}"; do
|
||||||
|
hash "$tool" && continue 2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo >&2 "Missing: $pkg. Please install this package."
|
||||||
|
(( failed++ ))
|
||||||
done
|
done
|
||||||
|
|
||||||
return $failed
|
return $failed
|
||||||
@@ -50,7 +58,7 @@ _initialize() {
|
|||||||
#
|
#
|
||||||
# Check if all tools needed for the default implementations are available.
|
# Check if all tools needed for the default implementations are available.
|
||||||
#
|
#
|
||||||
# By default, this will check for `libtool` (for libtoolize), `automake` (for aclocal) and `autoconf` (for autoreconf).
|
# By default, this will check for `libtool` (for libtoolize), `automake` (for aclocal), `autoconf` (for autoreconf) and make.
|
||||||
initialize_needs() { _initialize_needs "$@"; }
|
initialize_needs() { _initialize_needs "$@"; }
|
||||||
_initialize_needs() {
|
_initialize_needs() {
|
||||||
if [[ $platform = windows ]]; then
|
if [[ $platform = windows ]]; then
|
||||||
@@ -58,7 +66,7 @@ _initialize_needs() {
|
|||||||
export VSINSTALLDIR="${VSINSTALLDIR:-$(cd "$(cygpath -F 0x002a)/Microsoft Visual Studio"/*/*/Common7/.. && pwd)}"
|
export VSINSTALLDIR="${VSINSTALLDIR:-$(cd "$(cygpath -F 0x002a)/Microsoft Visual Studio"/*/*/Common7/.. && pwd)}"
|
||||||
[[ -e "$VSINSTALLDIR/Common7/Tools/VsMSBuildCmd.bat" ]] || { echo >&2 "Missing: msbuild. Please install 'Build Tools for Visual Studio'."; return 1; }
|
[[ -e "$VSINSTALLDIR/Common7/Tools/VsMSBuildCmd.bat" ]] || { echo >&2 "Missing: msbuild. Please install 'Build Tools for Visual Studio'."; return 1; }
|
||||||
else
|
else
|
||||||
needs libtool automake autoconf
|
needs libtool:libtoolize,glibtoolize automake autoconf make
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
source "${BASH_SOURCE%/*}/build_lib"
|
source "${BASH_SOURCE%/*}/build_lib"
|
||||||
|
|
||||||
|
finalize_merge() {
|
||||||
|
local prefix=$1 platform=$2; shift 2
|
||||||
|
local archs=( "$@" )
|
||||||
|
|
||||||
|
cp -a "src/libsodium/include" "$prefix/out"
|
||||||
|
|
||||||
|
_finalize_merge "$prefix" "$platform" "${archs[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
build libsodium windows
|
build libsodium windows
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<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="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"/>
|
<capability name="stacking Non-gravity area distributions on NSStackView" minToolsVersion="7.0" minSystemVersion="10.11"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
<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">
|
<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"/>
|
<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="contentRect" x="0.0" y="0.0" width="640" height="577"/>
|
||||||
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="640" height="577"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<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"/>
|
<rect key="frame" x="0.0" y="0.0" width="640" height="577"/>
|
||||||
</visualEffectView>
|
</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">
|
<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"/>
|
<rect key="frame" x="20" y="383" width="600" height="150"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ond-dT-x5d" userLabel="Site Password Label">
|
<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">
|
<shadow key="shadow" blurRadius="0.5">
|
||||||
<size key="offset" width="0.0" height="1"/>
|
<size key="offset" width="0.0" height="1"/>
|
||||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||||
@@ -73,7 +74,7 @@
|
|||||||
</connections>
|
</connections>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ia6-7b-dFr">
|
<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">
|
<shadow key="shadow" blurRadius="0.5">
|
||||||
<size key="offset" width="0.0" height="1"/>
|
<size key="offset" width="0.0" height="1"/>
|
||||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||||
@@ -186,7 +187,7 @@
|
|||||||
</contentFilters>
|
</contentFilters>
|
||||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="KeljXoleKowi9@" placeholderString="" id="WVV-EE-tkB">
|
<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"/>
|
<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"/>
|
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
<connections>
|
<connections>
|
||||||
@@ -223,13 +224,13 @@
|
|||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<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">
|
<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"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<size key="intercellSpacing" width="3" height="2"/>
|
<size key="intercellSpacing" width="3" height="2"/>
|
||||||
<color key="backgroundColor" white="1" alpha="0.0" colorSpace="deviceWhite"/>
|
<color key="backgroundColor" white="1" alpha="0.0" colorSpace="deviceWhite"/>
|
||||||
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
|
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
|
||||||
<tableColumns>
|
<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">
|
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left">
|
||||||
<font key="font" metaFont="smallSystem"/>
|
<font key="font" metaFont="smallSystem"/>
|
||||||
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
|
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
@@ -312,7 +313,7 @@
|
|||||||
</connections>
|
</connections>
|
||||||
</scrollView>
|
</scrollView>
|
||||||
<customView translatesAutoresizingMaskIntoConstraints="NO" id="nM8-O3-spM" customClass="MPGradientView">
|
<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>
|
<userDefinedRuntimeAttributes>
|
||||||
<userDefinedRuntimeAttribute type="color" keyPath="startingColor">
|
<userDefinedRuntimeAttribute type="color" keyPath="startingColor">
|
||||||
<color key="value" red="0.7019608021" green="0.7019608021" blue="0.7019608021" alpha="0.0" colorSpace="calibratedRGB"/>
|
<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>
|
</connections>
|
||||||
</textField>
|
</textField>
|
||||||
<stackView distribution="fill" orientation="horizontal" alignment="bottom" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pHt-gg-ZNX">
|
<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>
|
<subviews>
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1Qo-iG-CQt">
|
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1Qo-iG-CQt">
|
||||||
<rect key="frame" x="0.0" y="-1" width="85" height="19"/>
|
<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>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<stackView distribution="fill" orientation="vertical" alignment="centerX" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DT0-RU-3LT">
|
<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>
|
<subviews>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="uol-dE-I8H">
|
<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">
|
<shadow key="shadow" blurRadius="0.5">
|
||||||
<size key="offset" width="0.0" height="1"/>
|
<size key="offset" width="0.0" height="1"/>
|
||||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
<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>
|
</connections>
|
||||||
</textField>
|
</textField>
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="brI-fg-Kav">
|
<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">
|
<shadow key="shadow">
|
||||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||||
</shadow>
|
</shadow>
|
||||||
@@ -662,7 +663,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
|||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="R46-fx-n14">
|
<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">
|
<shadow key="shadow">
|
||||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||||
</shadow>
|
</shadow>
|
||||||
@@ -688,10 +689,10 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
|||||||
</connections>
|
</connections>
|
||||||
</button>
|
</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">
|
<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>
|
<subviews>
|
||||||
<stepper horizontalHuggingPriority="750" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="mcq-qD-yte">
|
<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">
|
<shadow key="shadow">
|
||||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||||
</shadow>
|
</shadow>
|
||||||
@@ -701,7 +702,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
|||||||
</connections>
|
</connections>
|
||||||
</stepper>
|
</stepper>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gyg-Fh-yn7">
|
<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">
|
<shadow key="shadow" blurRadius="0.5">
|
||||||
<size key="offset" width="0.0" height="1"/>
|
<size key="offset" width="0.0" height="1"/>
|
||||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
<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>
|
</connections>
|
||||||
</stackView>
|
</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">
|
<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>
|
<subviews>
|
||||||
<stepper horizontalHuggingPriority="750" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="XgA-Vl-CKh" userLabel="Counter Stepper">
|
<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">
|
<shadow key="shadow">
|
||||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
||||||
</shadow>
|
</shadow>
|
||||||
@@ -755,7 +756,7 @@ Use the arrows ⇅ to navigate the list or esc ⎋ to exit.</string>
|
|||||||
</connections>
|
</connections>
|
||||||
</stepper>
|
</stepper>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="NvO-kt-eZ2" userLabel="Counter Field">
|
<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">
|
<shadow key="shadow" blurRadius="0.5">
|
||||||
<size key="offset" width="0.0" height="1"/>
|
<size key="offset" width="0.0" height="1"/>
|
||||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
<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>
|
</customSpacing>
|
||||||
</stackView>
|
</stackView>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="luC-0j-BeV">
|
<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">
|
<shadow key="shadow" blurRadius="0.5">
|
||||||
<size key="offset" width="0.0" height="1"/>
|
<size key="offset" width="0.0" height="1"/>
|
||||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
<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>
|
</connections>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gjx-bt-fKM">
|
<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">
|
<shadow key="shadow" blurRadius="0.5">
|
||||||
<size key="offset" width="0.0" height="1"/>
|
<size key="offset" width="0.0" height="1"/>
|
||||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
<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>
|
</connections>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="dbM-ja-dKO" userLabel="Version Tip">
|
<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">
|
<shadow key="shadow" blurRadius="0.5">
|
||||||
<size key="offset" width="0.0" height="1"/>
|
<size key="offset" width="0.0" height="1"/>
|
||||||
<color key="color" name="controlLightHighlightColor" catalog="System" colorSpace="catalog"/>
|
<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"/>
|
<binding destination="-2" name="contentArray" keyPath="sites" id="c96-Dv-HK1"/>
|
||||||
</connections>
|
</connections>
|
||||||
</arrayController>
|
</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"/>
|
<rect key="frame" x="0.0" y="0.0" width="416" height="296"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<view key="contentView" id="hAc-y9-IMT">
|
<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"/>
|
<constraint firstItem="3fr-Fd-pxx" firstAttribute="top" secondItem="hAc-y9-IMT" secondAttribute="top" constant="8" id="xVT-HC-qsE"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</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"/>
|
<point key="canvasLocation" x="333" y="125"/>
|
||||||
</box>
|
</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"/>
|
<rect key="frame" x="0.0" y="0.0" width="416" height="180"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<view key="contentView" wantsLayer="YES" id="8Ep-zH-Nzv">
|
<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 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"/>
|
<constraint firstItem="12d-V9-LDB" firstAttribute="leading" secondItem="hi3-SX-Td3" secondAttribute="leading" constant="12" id="oPv-4N-T9I"/>
|
||||||
</constraints>
|
</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"/>
|
<point key="canvasLocation" x="333" y="491"/>
|
||||||
</box>
|
</box>
|
||||||
</objects>
|
</objects>
|
||||||
|
|||||||
@@ -14,14 +14,13 @@ artifacts {
|
|||||||
from 'lib'
|
from 'lib'
|
||||||
|
|
||||||
components.withType( ComponentWithRuntimeFile ) {
|
components.withType( ComponentWithRuntimeFile ) {
|
||||||
if (isOptimized()) {
|
if (optimized)
|
||||||
from runtimeFile, {
|
from runtimeFile, {
|
||||||
into standardOperatingSystem( targetPlatform ) + '/' + standardArchitecture( targetPlatform )
|
into standardOperatingSystem( targetPlatform ) + '/' + standardArchitecture( targetPlatform )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
library {
|
library {
|
||||||
baseName.set( 'mpw' )
|
baseName.set( 'mpw' )
|
||||||
@@ -36,7 +35,7 @@ library {
|
|||||||
}
|
}
|
||||||
withType( GccCompatibleToolChain ) {
|
withType( GccCompatibleToolChain ) {
|
||||||
eachPlatform {
|
eachPlatform {
|
||||||
cppCompiler.withArguments { addAll( ['-x', 'c', '-O3', '-std=c11', '-Werror', '-DMPW_SODIUM=1'] ) }
|
cppCompiler.withArguments { addAll( ['-x', 'c', '-O3', '-Werror', '-DMPW_SODIUM=1'] ) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,27 +54,29 @@ library {
|
|||||||
from files( new File( Jvm.current().javaHome, 'include' ) ) { first().eachDir { from it } }
|
from files( new File( Jvm.current().javaHome, 'include' ) ) { first().eachDir { from it } }
|
||||||
}
|
}
|
||||||
|
|
||||||
binaries.configureEach {
|
binaries.whenElementFinalized {
|
||||||
|
project.dependencies {
|
||||||
def system = standardOperatingSystem( targetPlatform )
|
def system = standardOperatingSystem( targetPlatform )
|
||||||
|
|
||||||
project.dependencies {
|
// libsodium
|
||||||
add( linkLibraries.name,
|
archive.dependsOn project.tasks.maybeCreate( "build_libsodium-${system}", Exec ).configure {
|
||||||
fileTree( "$rootDir/../lib/libsodium/build-${system}~/out/lib" ) )
|
|
||||||
}
|
|
||||||
|
|
||||||
archive.dependsOn project.tasks.maybeCreate( "build_libsodium-${system}", Exec.class ).configure {
|
|
||||||
commandLine 'bash', "$rootDir/../lib/bin/build_libsodium-${system}"
|
commandLine 'bash', "$rootDir/../lib/bin/build_libsodium-${system}"
|
||||||
privateHeaders.from "$rootDir/../lib/libsodium/build-${system}~/out/include"
|
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.class ).configure {
|
clean.dependsOn project.tasks.maybeCreate( "clean_libsodium-${system}", Exec ).configure {
|
||||||
commandLine 'bash', "$rootDir/../lib/bin/build_libsodium-${system}", 'clean'
|
commandLine 'bash', "$rootDir/../lib/bin/build_libsodium-${system}", 'clean'
|
||||||
}
|
}
|
||||||
archive.dependsOn project.tasks.maybeCreate( "build_libjson-c-${system}", Exec.class ).configure {
|
|
||||||
|
// libjson-c
|
||||||
|
/*archive.dependsOn project.tasks.maybeCreate( "build_libjson-c-${system}", Exec ).configure {
|
||||||
commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-${system}"
|
commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-${system}"
|
||||||
privateHeaders.from "$rootDir/../lib/libjson-c/build-${system}~/out/include"
|
privateHeaders.from "$rootDir/../lib/libjson-c/build-${system}~/out/include"
|
||||||
|
add( linkLibraries.name, fileTree( "$rootDir/../lib/libjson-c/build-${system}~/out/lib" ) )
|
||||||
}
|
}
|
||||||
clean.dependsOn project.tasks.maybeCreate( "clean_libjson-c-${system}", Exec.class ).configure {
|
clean.dependsOn project.tasks.maybeCreate( "clean_libjson-c-${system}", Exec ).configure {
|
||||||
commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-${system}", 'clean'
|
commandLine 'bash', "$rootDir/../lib/bin/build_libjson-c-${system}", 'clean'
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
platform-independent/c/core/lib/windows/x86/mpw.dll
Executable file
@@ -1,12 +1,13 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "mpw-jni.h"
|
#include "java/com_lyndir_masterpassword_impl_MPAlgorithmV0.h"
|
||||||
|
|
||||||
#include "mpw-algorithm.h"
|
#include "mpw-algorithm.h"
|
||||||
#include "mpw-util.h"
|
#include "mpw-util.h"
|
||||||
|
|
||||||
// TODO: We may need to zero the jbytes safely.
|
// TODO: We may need to zero the jbytes safely.
|
||||||
|
|
||||||
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
JNIEnv* env;
|
JNIEnv* env;
|
||||||
if ((*vm)->GetEnv( vm, (void **)&env, JNI_VERSION_1_6 ) != JNI_OK)
|
if ((*vm)->GetEnv( vm, (void **)&env, JNI_VERSION_1_6 ) != JNI_OK)
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ char *mpw_get_token(const char **in, const char *eol, char *delim) {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
time_t mpw_mktime(
|
time_t mpw_timegm(const char *time) {
|
||||||
const char *time) {
|
|
||||||
|
|
||||||
// TODO: Support for parsing non-UTC time strings
|
// TODO: Support for parsing non-UTC time strings
|
||||||
|
// Parse time as a UTC timestamp, into a tm.
|
||||||
struct tm tm = { .tm_isdst = -1 };
|
struct tm tm = { .tm_isdst = -1 };
|
||||||
if (time && sscanf( time, "%4d-%2d-%2dT%2d:%2d:%2dZ",
|
if (time && sscanf( time, "%4d-%2d-%2dT%2d:%2d:%2dZ",
|
||||||
&tm.tm_year, &tm.tm_mon, &tm.tm_mday,
|
&tm.tm_year, &tm.tm_mon, &tm.tm_mday,
|
||||||
@@ -46,8 +46,10 @@ time_t mpw_mktime(
|
|||||||
tm.tm_year -= 1900; // tm_year 0 = rfc3339 year 1900
|
tm.tm_year -= 1900; // tm_year 0 = rfc3339 year 1900
|
||||||
tm.tm_mon -= 1; // tm_mon 0 = rfc3339 month 1
|
tm.tm_mon -= 1; // tm_mon 0 = rfc3339 month 1
|
||||||
|
|
||||||
// mktime converts tm to local, setting tm_gmtoff; use it to offset the result back to UTC.
|
// mktime interprets tm as being local, we need to offset back to UTC (timegm/tm_gmtoff are non-standard).
|
||||||
return mktime( &tm ) + tm.tm_gmtoff;
|
time_t local_time = mktime( &tm ), local_dst = tm.tm_isdst > 0? 3600: 0;
|
||||||
|
time_t gmtoff = local_time + local_dst - mktime( gmtime( &local_time ) );
|
||||||
|
return local_time + gmtoff;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
char *mpw_get_token(
|
char *mpw_get_token(
|
||||||
const char **in, const char *eol, char *delim);
|
const char **in, const char *eol, char *delim);
|
||||||
/** Convert an RFC 3339 time string into epoch time. */
|
/** Convert an RFC 3339 time string into epoch time. */
|
||||||
time_t mpw_mktime(
|
time_t mpw_timegm(
|
||||||
const char *time);
|
const char *time);
|
||||||
|
|
||||||
/// JSON parsing.
|
/// JSON parsing.
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ static void mpw_marshal_read_flat_info(
|
|||||||
if (strcmp( headerName, "Passwords" ) == 0)
|
if (strcmp( headerName, "Passwords" ) == 0)
|
||||||
info->redacted = strcmp( headerValue, "VISIBLE" ) != 0;
|
info->redacted = strcmp( headerValue, "VISIBLE" ) != 0;
|
||||||
if (strcmp( headerName, "Date" ) == 0)
|
if (strcmp( headerName, "Date" ) == 0)
|
||||||
info->date = mpw_mktime( headerValue );
|
info->date = mpw_timegm( headerValue );
|
||||||
|
|
||||||
mpw_free_strings( &headerName, &headerValue, NULL );
|
mpw_free_strings( &headerName, &headerValue, NULL );
|
||||||
continue;
|
continue;
|
||||||
@@ -580,7 +580,7 @@ static MPMarshalledUser *mpw_marshal_read_flat(
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
MPAlgorithmVersion siteAlgorithm = (MPAlgorithmVersion)value;
|
MPAlgorithmVersion siteAlgorithm = (MPAlgorithmVersion)value;
|
||||||
time_t siteLastUsed = mpw_mktime( str_lastUsed );
|
time_t siteLastUsed = mpw_timegm( str_lastUsed );
|
||||||
if (!siteLastUsed) {
|
if (!siteLastUsed) {
|
||||||
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid site last used: %s: %s", siteName, str_lastUsed ) };
|
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid site last used: %s: %s", siteName, str_lastUsed ) };
|
||||||
return NULL;
|
return NULL;
|
||||||
@@ -650,7 +650,7 @@ static void mpw_marshal_read_json_info(
|
|||||||
if (fileFormat < 1)
|
if (fileFormat < 1)
|
||||||
return;
|
return;
|
||||||
info->redacted = mpw_get_json_boolean( json_file, "export.redacted", true );
|
info->redacted = mpw_get_json_boolean( json_file, "export.redacted", true );
|
||||||
info->date = mpw_mktime( mpw_get_json_string( json_file, "export.date", NULL ) );
|
info->date = mpw_timegm( mpw_get_json_string( json_file, "export.date", NULL ) );
|
||||||
|
|
||||||
// Section: "user"
|
// Section: "user"
|
||||||
info->algorithm = (MPAlgorithmVersion)mpw_get_json_int( json_file, "user.algorithm", MPAlgorithmVersionCurrent );
|
info->algorithm = (MPAlgorithmVersion)mpw_get_json_int( json_file, "user.algorithm", MPAlgorithmVersionCurrent );
|
||||||
@@ -707,7 +707,7 @@ static MPMarshalledUser *mpw_marshal_read_json(
|
|||||||
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid user default type: %u", defaultType ) };
|
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid user default type: %u", defaultType ) };
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
time_t lastUsed = mpw_mktime( str_lastUsed );
|
time_t lastUsed = mpw_timegm( str_lastUsed );
|
||||||
if (!lastUsed) {
|
if (!lastUsed) {
|
||||||
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid user last used: %s", str_lastUsed ) };
|
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid user last used: %s", str_lastUsed ) };
|
||||||
return NULL;
|
return NULL;
|
||||||
@@ -760,7 +760,7 @@ static MPMarshalledUser *mpw_marshal_read_json(
|
|||||||
MPResultType siteLoginType = (MPResultType)mpw_get_json_int( json_site.val, "login_type", MPResultTypeTemplateName );
|
MPResultType siteLoginType = (MPResultType)mpw_get_json_int( json_site.val, "login_type", MPResultTypeTemplateName );
|
||||||
unsigned int siteUses = (unsigned int)mpw_get_json_int( json_site.val, "uses", 0 );
|
unsigned int siteUses = (unsigned int)mpw_get_json_int( json_site.val, "uses", 0 );
|
||||||
str_lastUsed = mpw_get_json_string( json_site.val, "last_used", NULL );
|
str_lastUsed = mpw_get_json_string( json_site.val, "last_used", NULL );
|
||||||
time_t siteLastUsed = mpw_mktime( str_lastUsed );
|
time_t siteLastUsed = mpw_timegm( str_lastUsed );
|
||||||
if (!siteLastUsed) {
|
if (!siteLastUsed) {
|
||||||
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid site last used: %s: %s", siteName, str_lastUsed ) };
|
*error = (MPMarshalError){ MPMarshalErrorIllegal, mpw_str( "Invalid site last used: %s: %s", siteName, str_lastUsed ) };
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ plugins {
|
|||||||
|
|
||||||
description = 'Master Password Algorithm Implementation'
|
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 {
|
configurations {
|
||||||
lib
|
lib
|
||||||
}
|
}
|
||||||
@@ -24,17 +29,7 @@ processResources {
|
|||||||
into new File( processResources.outputs.files.singleFile, "lib" )
|
into new File( processResources.outputs.files.singleFile, "lib" )
|
||||||
|
|
||||||
dependsOn configurations.lib {
|
dependsOn configurations.lib {
|
||||||
files.each { libFile ->
|
files.each { libFile -> from( zipTree( 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 )
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import com.lyndir.lhunath.opal.system.MessageDigests;
|
|||||||
import com.lyndir.masterpassword.impl.*;
|
import com.lyndir.masterpassword.impl.*;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
|
||||||
@@ -88,46 +89,55 @@ public abstract class MPAlgorithm {
|
|||||||
/**
|
/**
|
||||||
* The linear version identifier of this algorithm's implementation.
|
* The linear version identifier of this algorithm's implementation.
|
||||||
*/
|
*/
|
||||||
|
@Nonnull
|
||||||
public abstract Version version();
|
public abstract Version version();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mpw: defaults: initial counter value.
|
* mpw: defaults: initial counter value.
|
||||||
*/
|
*/
|
||||||
|
@Nonnull
|
||||||
public abstract UnsignedInteger mpw_default_counter();
|
public abstract UnsignedInteger mpw_default_counter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mpw: defaults: password result type.
|
* mpw: defaults: password result type.
|
||||||
*/
|
*/
|
||||||
|
@Nonnull
|
||||||
public abstract MPResultType mpw_default_result_type();
|
public abstract MPResultType mpw_default_result_type();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mpw: defaults: login result type.
|
* mpw: defaults: login result type.
|
||||||
*/
|
*/
|
||||||
|
@Nonnull
|
||||||
public abstract MPResultType mpw_default_login_type();
|
public abstract MPResultType mpw_default_login_type();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mpw: defaults: answer result type.
|
* mpw: defaults: answer result type.
|
||||||
*/
|
*/
|
||||||
|
@Nonnull
|
||||||
public abstract MPResultType mpw_default_answer_type();
|
public abstract MPResultType mpw_default_answer_type();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mpw: Input character encoding.
|
* mpw: Input character encoding.
|
||||||
*/
|
*/
|
||||||
|
@Nonnull
|
||||||
public abstract Charset mpw_charset();
|
public abstract Charset mpw_charset();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mpw: Platform-agnostic byte order.
|
* mpw: Platform-agnostic byte order.
|
||||||
*/
|
*/
|
||||||
|
@Nonnull
|
||||||
public abstract ByteOrder mpw_byteOrder();
|
public abstract ByteOrder mpw_byteOrder();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mpw: Key ID hash.
|
* mpw: Key ID hash.
|
||||||
*/
|
*/
|
||||||
|
@Nonnull
|
||||||
public abstract MessageDigests mpw_hash();
|
public abstract MessageDigests mpw_hash();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mpw: Site digest.
|
* mpw: Site digest.
|
||||||
*/
|
*/
|
||||||
|
@Nonnull
|
||||||
public abstract MessageAuthenticationDigests mpw_digest();
|
public abstract MessageAuthenticationDigests mpw_digest();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,12 +177,16 @@ public abstract class MPAlgorithm {
|
|||||||
|
|
||||||
// Utilities
|
// Utilities
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
protected abstract byte[] toBytes(int number);
|
protected abstract byte[] toBytes(int number);
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
protected abstract byte[] toBytes(UnsignedInteger number);
|
protected abstract byte[] toBytes(UnsignedInteger number);
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
protected abstract byte[] toBytes(char[] characters);
|
protected abstract byte[] toBytes(char[] characters);
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
protected abstract byte[] toID(byte[] bytes);
|
protected abstract byte[] toID(byte[] bytes);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import com.lyndir.lhunath.opal.system.logging.Logger;
|
|||||||
import java.nio.*;
|
import java.nio.*;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,6 +84,10 @@ public class MPIdenticon {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getHTML() {
|
||||||
|
return strf( "<span style='color: %s'>%s</span>", color.getCSS(), text );
|
||||||
|
}
|
||||||
|
|
||||||
public Color getColor() {
|
public Color getColor() {
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
@@ -94,6 +99,15 @@ public class MPIdenticon {
|
|||||||
BLUE,
|
BLUE,
|
||||||
MAGENTA,
|
MAGENTA,
|
||||||
CYAN,
|
CYAN,
|
||||||
MONO
|
MONO {
|
||||||
|
@Override
|
||||||
|
public String getCSS() {
|
||||||
|
return "inherit";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public String getCSS() {
|
||||||
|
return name().toLowerCase( Locale.ROOT );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ public class MPMasterKey {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param masterPassword The characters of the user's master password.
|
* @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")
|
@SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
|
||||||
public MPMasterKey(final String fullName, final char[] masterPassword) {
|
public MPMasterKey(final String fullName, final char[] masterPassword) {
|
||||||
@@ -82,11 +83,15 @@ public class MPMasterKey {
|
|||||||
Arrays.fill( masterPassword, (char) 0 );
|
Arrays.fill( masterPassword, (char) 0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return !invalidated;
|
||||||
|
}
|
||||||
|
|
||||||
private byte[] masterKey(final MPAlgorithm algorithm)
|
private byte[] masterKey(final MPAlgorithm algorithm)
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||||
Preconditions.checkArgument( masterPassword.length > 0 );
|
Preconditions.checkArgument( masterPassword.length > 0 );
|
||||||
|
|
||||||
if (invalidated)
|
if (!isValid())
|
||||||
throw new MPKeyUnavailableException( "Master key was invalidated." );
|
throw new MPKeyUnavailableException( "Master key was invalidated." );
|
||||||
|
|
||||||
byte[] masterKey = keyByVersion.get( algorithm.version() );
|
byte[] masterKey = keyByVersion.get( algorithm.version() );
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public enum MPResultType {
|
|||||||
/**
|
/**
|
||||||
* 16: pg^VMAUBk5x3p%HP%i4=
|
* 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" ),
|
ImmutableList.of( new MPTemplate( "anoxxxxxxxxxxxxxxxxx" ),
|
||||||
new MPTemplate( "axxxxxxxxxxxxxxxxxno" ) ), //
|
new MPTemplate( "axxxxxxxxxxxxxxxxxno" ) ), //
|
||||||
MPResultTypeClass.Template, 0x0 ),
|
MPResultTypeClass.Template, 0x0 ),
|
||||||
@@ -49,7 +49,7 @@ public enum MPResultType {
|
|||||||
/**
|
/**
|
||||||
* 17: BiroYena8:Kixa
|
* 17: BiroYena8:Kixa
|
||||||
*/
|
*/
|
||||||
GeneratedLong( "long", "Copy-friendly, 14 characters, contains symbols.", //
|
GeneratedLong( "long", "Long Password", "BiroYena8:Kixa", "Copy-friendly, 14 characters, contains symbols.", //
|
||||||
ImmutableList.of( new MPTemplate( "CvcvnoCvcvCvcv" ), new MPTemplate( "CvcvCvcvnoCvcv" ),
|
ImmutableList.of( new MPTemplate( "CvcvnoCvcvCvcv" ), new MPTemplate( "CvcvCvcvnoCvcv" ),
|
||||||
new MPTemplate( "CvcvCvcvCvcvno" ), new MPTemplate( "CvccnoCvcvCvcv" ),
|
new MPTemplate( "CvcvCvcvCvcvno" ), new MPTemplate( "CvccnoCvcvCvcv" ),
|
||||||
new MPTemplate( "CvccCvcvnoCvcv" ), new MPTemplate( "CvccCvcvCvcvno" ),
|
new MPTemplate( "CvccCvcvnoCvcv" ), new MPTemplate( "CvccCvcvCvcvno" ),
|
||||||
@@ -66,7 +66,7 @@ public enum MPResultType {
|
|||||||
/**
|
/**
|
||||||
* 18: BirSuj0-
|
* 18: BirSuj0-
|
||||||
*/
|
*/
|
||||||
GeneratedMedium( "medium", "Copy-friendly, 8 characters, contains symbols.", //
|
GeneratedMedium( "medium", "Medium Password", "BirSuj0-", "Copy-friendly, 8 characters, contains symbols.", //
|
||||||
ImmutableList.of( new MPTemplate( "CvcnoCvc" ),
|
ImmutableList.of( new MPTemplate( "CvcnoCvc" ),
|
||||||
new MPTemplate( "CvcCvcno" ) ), //
|
new MPTemplate( "CvcCvcno" ) ), //
|
||||||
MPResultTypeClass.Template, 0x2 ),
|
MPResultTypeClass.Template, 0x2 ),
|
||||||
@@ -74,14 +74,14 @@ public enum MPResultType {
|
|||||||
/**
|
/**
|
||||||
* 19: Bir8
|
* 19: Bir8
|
||||||
*/
|
*/
|
||||||
GeneratedShort( "short", "Copy-friendly, 4 characters, no symbols.", //
|
GeneratedShort( "short", "Short Password", "Bir8", "Copy-friendly, 4 characters, no symbols.", //
|
||||||
ImmutableList.of( new MPTemplate( "Cvcn" ) ), //
|
ImmutableList.of( new MPTemplate( "Cvcn" ) ), //
|
||||||
MPResultTypeClass.Template, 0x3 ),
|
MPResultTypeClass.Template, 0x3 ),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 20: pO98MoD0
|
* 20: pO98MoD0
|
||||||
*/
|
*/
|
||||||
GeneratedBasic( "basic", "8 characters, no symbols.", //
|
GeneratedBasic( "basic", "Basic Password", "pO98MoD0", "8 characters, no symbols.", //
|
||||||
ImmutableList.of( new MPTemplate( "aaanaaan" ),
|
ImmutableList.of( new MPTemplate( "aaanaaan" ),
|
||||||
new MPTemplate( "aannaaan" ),
|
new MPTemplate( "aannaaan" ),
|
||||||
new MPTemplate( "aaannaaa" ) ), //
|
new MPTemplate( "aaannaaa" ) ), //
|
||||||
@@ -90,21 +90,21 @@ public enum MPResultType {
|
|||||||
/**
|
/**
|
||||||
* 21: 2798
|
* 21: 2798
|
||||||
*/
|
*/
|
||||||
GeneratedPIN( "pin", "4 numbers.", //
|
GeneratedPIN( "pin", "PIN Code", "2798", "4 numbers.", //
|
||||||
ImmutableList.of( new MPTemplate( "nnnn" ) ), //
|
ImmutableList.of( new MPTemplate( "nnnn" ) ), //
|
||||||
MPResultTypeClass.Template, 0x5 ),
|
MPResultTypeClass.Template, 0x5 ),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 30: birsujano
|
* 30: birsujano
|
||||||
*/
|
*/
|
||||||
GeneratedName( "name", "9 letter name.", //
|
GeneratedName( "name", "Name", "birsujano", "9 letter name.", //
|
||||||
ImmutableList.of( new MPTemplate( "cvccvcvcv" ) ), //
|
ImmutableList.of( new MPTemplate( "cvccvcvcv" ) ), //
|
||||||
MPResultTypeClass.Template, 0xE ),
|
MPResultTypeClass.Template, 0xE ),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 31: bir yennoquce fefi
|
* 31: bir yennoquce fefi
|
||||||
*/
|
*/
|
||||||
GeneratedPhrase( "phrase", "20 character sentence.", //
|
GeneratedPhrase( "phrase", "Phrase", "bir yennoquce fefi", "20 character sentence.", //
|
||||||
ImmutableList.of( new MPTemplate( "cvcc cvc cvccvcv cvc" ),
|
ImmutableList.of( new MPTemplate( "cvcc cvc cvccvcv cvc" ),
|
||||||
new MPTemplate( "cvc cvccvcvcv cvcv" ),
|
new MPTemplate( "cvc cvccvcvcv cvcv" ),
|
||||||
new MPTemplate( "cv cvccv cvc cvcvccv" ) ), //
|
new MPTemplate( "cv cvccv cvc cvcvccv" ) ), //
|
||||||
@@ -113,37 +113,44 @@ public enum MPResultType {
|
|||||||
/**
|
/**
|
||||||
* 1056: Custom saved password.
|
* 1056: Custom saved password.
|
||||||
*/
|
*/
|
||||||
StoredPersonal( "personal", "AES-encrypted, exportable.", //
|
StoredPersonal( "personal", "Saved Password", null, "AES-encrypted, exportable.", //
|
||||||
ImmutableList.<MPTemplate>of(), //
|
ImmutableList.<MPTemplate>of(), //
|
||||||
MPResultTypeClass.Stateful, 0x0, MPSiteFeature.ExportContent ),
|
MPResultTypeClass.Stateful, 0x0, MPSiteFeature.ExportContent ),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 2081: Custom saved password that should not be exported from the device.
|
* 2081: Custom saved password that should not be exported from the device.
|
||||||
*/
|
*/
|
||||||
StoredDevicePrivate( "device", "AES-encrypted, not exported.", //
|
StoredDevicePrivate( "device", "Private Password", null, "AES-encrypted, not exported.", //
|
||||||
ImmutableList.<MPTemplate>of(), //
|
ImmutableList.<MPTemplate>of(), //
|
||||||
MPResultTypeClass.Stateful, 0x1, MPSiteFeature.DevicePrivate ),
|
MPResultTypeClass.Stateful, 0x1, MPSiteFeature.DevicePrivate ),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 4160: Derive a unique binary key.
|
* 4160: Derive a unique binary key.
|
||||||
*/
|
*/
|
||||||
DeriveKey( "key", "Encryption key.", //
|
DeriveKey( "key", "Binary Key", null, "Encryption key.", //
|
||||||
ImmutableList.<MPTemplate>of(), //
|
ImmutableList.<MPTemplate>of(), //
|
||||||
MPResultTypeClass.Derive, 0x0, MPSiteFeature.Alternative );
|
MPResultTypeClass.Derive, 0x0, MPSiteFeature.Alternative );
|
||||||
|
|
||||||
static final Logger logger = Logger.get( MPResultType.class );
|
static final Logger logger = Logger.get( MPResultType.class );
|
||||||
|
|
||||||
private final String shortName;
|
private final String shortName;
|
||||||
|
private final String longName;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final String sample;
|
||||||
private final String description;
|
private final String description;
|
||||||
private final List<MPTemplate> templates;
|
private final List<MPTemplate> templates;
|
||||||
private final MPResultTypeClass typeClass;
|
private final MPResultTypeClass typeClass;
|
||||||
private final int typeIndex;
|
private final int typeIndex;
|
||||||
private final ImmutableSet<MPSiteFeature> typeFeatures;
|
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) {
|
final MPResultTypeClass typeClass, final int typeIndex, final MPSiteFeature... typeFeatures) {
|
||||||
|
|
||||||
this.shortName = shortName;
|
this.shortName = shortName;
|
||||||
|
this.longName = longName;
|
||||||
|
this.sample = sample;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.templates = templates;
|
this.templates = templates;
|
||||||
this.typeClass = typeClass;
|
this.typeClass = typeClass;
|
||||||
@@ -160,6 +167,15 @@ public enum MPResultType {
|
|||||||
return shortName;
|
return shortName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getLongName() {
|
||||||
|
return longName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getSample() {
|
||||||
|
return sample;
|
||||||
|
}
|
||||||
|
|
||||||
public String getDescription() {
|
public String getDescription() {
|
||||||
|
|
||||||
return description;
|
return description;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import com.lyndir.masterpassword.*;
|
|||||||
import java.nio.*;
|
import java.nio.*;
|
||||||
import java.nio.charset.*;
|
import java.nio.charset.*;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
|
||||||
@@ -122,46 +123,55 @@ public class MPAlgorithmV0 extends MPAlgorithm {
|
|||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public Version version() {
|
public Version version() {
|
||||||
return MPAlgorithm.Version.V0;
|
return MPAlgorithm.Version.V0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public UnsignedInteger mpw_default_counter() {
|
public UnsignedInteger mpw_default_counter() {
|
||||||
return UnsignedInteger.ONE;
|
return UnsignedInteger.ONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public MPResultType mpw_default_result_type() {
|
public MPResultType mpw_default_result_type() {
|
||||||
return MPResultType.GeneratedLong;
|
return MPResultType.GeneratedLong;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public MPResultType mpw_default_login_type() {
|
public MPResultType mpw_default_login_type() {
|
||||||
return MPResultType.GeneratedName;
|
return MPResultType.GeneratedName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public MPResultType mpw_default_answer_type() {
|
public MPResultType mpw_default_answer_type() {
|
||||||
return MPResultType.GeneratedPhrase;
|
return MPResultType.GeneratedPhrase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public Charset mpw_charset() {
|
public Charset mpw_charset() {
|
||||||
return Charsets.UTF_8;
|
return Charsets.UTF_8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public ByteOrder mpw_byteOrder() {
|
public ByteOrder mpw_byteOrder() {
|
||||||
return ByteOrder.BIG_ENDIAN;
|
return ByteOrder.BIG_ENDIAN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public MessageDigests mpw_hash() {
|
public MessageDigests mpw_hash() {
|
||||||
return MessageDigests.SHA256;
|
return MessageDigests.SHA256;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public MessageAuthenticationDigests mpw_digest() {
|
public MessageAuthenticationDigests mpw_digest() {
|
||||||
return MessageAuthenticationDigests.HmacSHA256;
|
return MessageAuthenticationDigests.HmacSHA256;
|
||||||
@@ -211,16 +221,19 @@ public class MPAlgorithmV0 extends MPAlgorithm {
|
|||||||
|
|
||||||
// Utilities
|
// Utilities
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public byte[] toBytes(final int number) {
|
public byte[] toBytes(final int number) {
|
||||||
return ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( mpw_byteOrder() ).putInt( number ).array();
|
return ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( mpw_byteOrder() ).putInt( number ).array();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public byte[] toBytes(final UnsignedInteger number) {
|
public byte[] toBytes(final UnsignedInteger number) {
|
||||||
return ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( mpw_byteOrder() ).putInt( number.intValue() ).array();
|
return ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( mpw_byteOrder() ).putInt( number.intValue() ).array();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public byte[] toBytes(final char[] characters) {
|
public byte[] toBytes(final char[] characters) {
|
||||||
ByteBuffer byteBuffer = mpw_charset().encode( CharBuffer.wrap( characters ) );
|
ByteBuffer byteBuffer = mpw_charset().encode( CharBuffer.wrap( characters ) );
|
||||||
@@ -232,6 +245,7 @@ public class MPAlgorithmV0 extends MPAlgorithm {
|
|||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public byte[] toID(final byte[] bytes) {
|
public byte[] toID(final byte[] bytes) {
|
||||||
return mpw_hash().of( bytes );
|
return mpw_hash().of( bytes );
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
package com.lyndir.masterpassword.impl;
|
package com.lyndir.masterpassword.impl;
|
||||||
|
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +30,7 @@ public class MPAlgorithmV1 extends MPAlgorithmV0 {
|
|||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public Version version() {
|
public Version version() {
|
||||||
return MPAlgorithm.Version.V1;
|
return MPAlgorithm.Version.V1;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
package com.lyndir.masterpassword.impl;
|
package com.lyndir.masterpassword.impl;
|
||||||
|
|
||||||
import com.lyndir.masterpassword.MPAlgorithm;
|
import com.lyndir.masterpassword.MPAlgorithm;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +30,7 @@ public class MPAlgorithmV2 extends MPAlgorithmV1 {
|
|||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public Version version() {
|
public Version version() {
|
||||||
return MPAlgorithm.Version.V2;
|
return MPAlgorithm.Version.V2;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
package com.lyndir.masterpassword.impl;
|
package com.lyndir.masterpassword.impl;
|
||||||
|
|
||||||
import com.lyndir.masterpassword.MPAlgorithm;
|
import com.lyndir.masterpassword.MPAlgorithm;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +30,7 @@ public class MPAlgorithmV3 extends MPAlgorithmV2 {
|
|||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public Version version() {
|
public Version version() {
|
||||||
return MPAlgorithm.Version.V3;
|
return MPAlgorithm.Version.V3;
|
||||||
|
|||||||
@@ -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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* @author lhunath, 2018-07-25
|
||||||
|
*/
|
||||||
|
@ParametersAreNonnullByDefault
|
||||||
|
package com.lyndir.masterpassword.util;
|
||||||
|
|
||||||
|
import javax.annotation.ParametersAreNonnullByDefault;
|
||||||
@@ -5,32 +5,37 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
description = 'Master Password GUI'
|
description = 'Master Password GUI'
|
||||||
mainClassName = 'com.lyndir.masterpassword.gui.GUI'
|
mainClassName = 'com.lyndir.masterpassword.gui.MasterPassword'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation group: 'com.lyndir.lhunath.opal', name: 'opal-system', version: '1.7-p2'
|
implementation group: 'com.lyndir.lhunath.opal', name: 'opal-system', version: '1.7-p2'
|
||||||
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.1.2'
|
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.1.2'
|
||||||
implementation group: 'com.yuvimasory', name: 'orange-extensions', version: '1.3.0'
|
implementation group: 'com.yuvimasory', name: 'orange-extensions', version: '1.3.0'
|
||||||
|
implementation group: 'com.github.tulskiy', name: 'jkeymaster', version: '1.2'
|
||||||
|
|
||||||
compile project( ':masterpassword-model' )
|
compile project( ':masterpassword-model' )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// release with: STORE_PW=$(mpw masterpassword.keystore) KEY_PW_ANDROID=$(mpw masterpassword-android) gradle masterpassword-gui:shadowJar
|
// release with: STORE_PW=$(mpw masterpassword.keystore) KEY_PW_DESKTOP=$(mpw masterpassword-desktop) gradle clean masterpassword-gui:shadowJar
|
||||||
shadowJar.doLast {
|
shadowJar {
|
||||||
|
manifest {
|
||||||
|
attributes 'Implementation-Title': description
|
||||||
|
attributes 'Implementation-Version': version
|
||||||
|
}
|
||||||
|
doLast {
|
||||||
if (System.getenv( 'KEY_PW_DESKTOP' ) != null)
|
if (System.getenv( 'KEY_PW_DESKTOP' ) != null)
|
||||||
ant.signjar(
|
ant.signjar( jar: archivePath,
|
||||||
jar: archivePath,
|
|
||||||
alias: 'masterpassword-desktop',
|
alias: 'masterpassword-desktop',
|
||||||
keystore: 'masterpassword.keystore',
|
keystore: 'masterpassword.keystore',
|
||||||
storepass: System.getenv( 'STORE_PW' ),
|
storepass: System.getenv( 'STORE_PW' ),
|
||||||
keypass: System.getenv( 'KEY_PW_DESKTOP' ),
|
keypass: System.getenv( 'KEY_PW_DESKTOP' ),
|
||||||
preservelastmodified: 'true',
|
preservelastmodified: 'true',
|
||||||
destdir: '.'
|
destdir: '.' )
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
run {
|
run {
|
||||||
// I don't fully understand why this is necessary, but without it -Dmp.log.level is lost.
|
// I don't fully understand why this is necessary, but without it -Dmp.log.level is lost.
|
||||||
systemProperties = System.properties
|
//systemProperties = System.properties
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
package com.lyndir.masterpassword.gui;
|
package com.lyndir.masterpassword.gui;
|
||||||
|
|
||||||
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
|
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
|
||||||
import com.lyndir.masterpassword.model.MPConstants;
|
import com.lyndir.masterpassword.model.MPModelConstants;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +35,6 @@ public class Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkForUpdates() {
|
public boolean checkForUpdates() {
|
||||||
return ConversionUtils.toBoolean( System.getenv( MPConstants.env_checkUpdates ) ).orElse( true );
|
return ConversionUtils.toBoolean( System.getenv( MPModelConstants.env_checkUpdates ) ).orElse( true );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,143 +1,36 @@
|
|||||||
//==============================================================================
|
|
||||||
// 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;
|
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.logging.Logger;
|
||||||
import com.lyndir.lhunath.opal.system.util.TypeUtils;
|
import com.lyndir.masterpassword.gui.util.Platform;
|
||||||
import com.lyndir.masterpassword.gui.view.PasswordFrame;
|
import com.lyndir.masterpassword.gui.util.Res;
|
||||||
import com.lyndir.masterpassword.gui.view.UnlockFrame;
|
import com.lyndir.masterpassword.gui.view.MasterPasswordFrame;
|
||||||
import java.io.IOException;
|
import com.tulskiy.keymaster.common.Provider;
|
||||||
import java.io.InputStream;
|
import java.awt.*;
|
||||||
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 lhunath, 2018-07-28
|
||||||
*
|
|
||||||
* @author mbillemo
|
|
||||||
*/
|
*/
|
||||||
public class GUI implements UnlockFrame.SignInCallback {
|
public class GUI {
|
||||||
|
|
||||||
@SuppressWarnings("UnusedDeclaration")
|
|
||||||
private static final Logger logger = Logger.get( GUI.class );
|
private static final Logger logger = Logger.get( GUI.class );
|
||||||
|
|
||||||
private final UnlockFrame unlockFrame = new UnlockFrame( this );
|
private final MasterPasswordFrame frame = new MasterPasswordFrame();
|
||||||
private PasswordFrame<?, ?> passwordFrame;
|
|
||||||
|
|
||||||
public static void main(final String... args) {
|
public GUI() {
|
||||||
Thread.setDefaultUncaughtExceptionHandler(
|
Platform.get().installAppForegroundHandler( this::open );
|
||||||
(t, e) -> logger.err( e, "Uncaught: %s", e.getLocalizedMessage() ) );
|
Platform.get().installAppReopenHandler( this::open );
|
||||||
|
|
||||||
if (Config.get().checkForUpdates())
|
Provider.getCurrentProvider( true ).register( MPGuiConstants.ui_hotkey, hotKey -> open() );
|
||||||
checkUpdate();
|
|
||||||
|
|
||||||
// Try and set the system look & feel, if available.
|
|
||||||
try {
|
|
||||||
UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
|
|
||||||
}
|
|
||||||
catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
create().open();
|
public void open() {
|
||||||
}
|
Res.ui( () -> {
|
||||||
|
frame.setAlwaysOnTop( true );
|
||||||
private static GUI create() {
|
frame.setVisible( true );
|
||||||
try {
|
frame.setExtendedState( Frame.NORMAL );
|
||||||
// AppleGUI adds support for macOS features.
|
frame.setAlwaysOnTop( false );
|
||||||
Optional<Class<GUI>> appleGUI = TypeUtils.loadClass( "com.lyndir.masterpassword.gui.platform.mac.AppleGUI" );
|
Platform.get().requestForeground();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 );
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
//==============================================================================
|
||||||
|
// 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.Components;
|
||||||
|
import com.lyndir.masterpassword.model.MPUser;
|
||||||
|
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 MPUser<?> activeUser;
|
||||||
|
|
||||||
|
public static MasterPassword get() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addListener(final Listener listener) {
|
||||||
|
return listeners.add( listener );
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean removeListener(final Listener listener) {
|
||||||
|
return 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 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() );
|
||||||
|
}
|
||||||
|
catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a platform-specific GUI and open it.
|
||||||
|
new GUI().open();
|
||||||
|
|
||||||
|
// Check online to see if this version has been superseded.
|
||||||
|
if (Config.get().checkForUpdates())
|
||||||
|
get().checkUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Listener {
|
||||||
|
|
||||||
|
void onUserSelected(@Nullable MPUser<?> user);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,7 +33,7 @@ public class SwingExecutorService extends AbstractExecutorService {
|
|||||||
|
|
||||||
synchronized (pendingCommands) {
|
synchronized (pendingCommands) {
|
||||||
if (pendingCommands.isEmpty())
|
if (pendingCommands.isEmpty())
|
||||||
terminated.offer( true );
|
terminated.add( true );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ public class SwingExecutorService extends AbstractExecutorService {
|
|||||||
pendingCommands.remove( command );
|
pendingCommands.remove( command );
|
||||||
|
|
||||||
if (shutdown && pendingCommands.isEmpty())
|
if (shutdown && pendingCommands.isEmpty())
|
||||||
terminated.offer( true );
|
terminated.add( true );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,25 +29,15 @@ import javax.annotation.Nullable;
|
|||||||
/**
|
/**
|
||||||
* @author lhunath, 14-12-16
|
* @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 siteName) {
|
||||||
|
this( user, siteName, null, null, null, null );
|
||||||
public MPIncognitoSite(final MPIncognitoUser user, final String name) {
|
|
||||||
this( user, name, 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 MPAlgorithm algorithm, @Nullable final UnsignedInteger counter,
|
||||||
@Nullable final MPResultType resultType, @Nullable final MPResultType loginType) {
|
@Nullable final MPResultType resultType, @Nullable final MPResultType loginType) {
|
||||||
super( name, (algorithm == null)? user.getAlgorithm(): algorithm, counter, resultType, loginType );
|
super( user, siteName, (algorithm == null)? user.getAlgorithm(): algorithm, counter, resultType, loginType );
|
||||||
|
|
||||||
this.user = user;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public MPIncognitoUser getUser() {
|
|
||||||
return user;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,4 +37,9 @@ public class MPIncognitoUser extends MPBasicUser<MPIncognitoSite> {
|
|||||||
public byte[] getKeyID() {
|
public byte[] getKeyID() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MPIncognitoSite addSite(final String siteName) {
|
||||||
|
return addSite( new MPIncognitoSite( this, siteName ) );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.lyndir.masterpassword.gui.model;
|
||||||
|
|
||||||
|
import com.lyndir.masterpassword.model.*;
|
||||||
|
import com.lyndir.masterpassword.model.impl.*;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author lhunath, 2018-07-27
|
||||||
|
*/
|
||||||
|
public class MPNewSite extends MPBasicSite<MPUser<?>, MPQuestion> {
|
||||||
|
|
||||||
|
public MPNewSite(final MPUser<?> user, final String siteName) {
|
||||||
|
super( user, siteName );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package com.lyndir.masterpassword.gui.util;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
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 final List<E> model = new LinkedList<>();
|
||||||
|
@Nullable
|
||||||
|
private E selectedItem;
|
||||||
|
private JList<E> list;
|
||||||
|
@Nullable
|
||||||
|
private Consumer<E> selectionConsumer;
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
public static <E> CollectionListModel<E> copy(final E... elements) {
|
||||||
|
return copy( Arrays.asList( elements ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <E> CollectionListModel<E> copy(final Collection<? extends E> elements) {
|
||||||
|
CollectionListModel<E> model = new CollectionListModel<>();
|
||||||
|
synchronized (model) {
|
||||||
|
model.model.addAll( elements );
|
||||||
|
model.selectedItem = model.getElementAt( 0 );
|
||||||
|
model.fireIntervalAdded( model, 0, model.model.size() );
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized int getSize() {
|
||||||
|
return model.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
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({ "unchecked", "SuspiciousToArrayCall" })
|
||||||
|
public synchronized void set(final Collection<? extends E> elements) {
|
||||||
|
set( (E[]) elements.toArray( new Object[0] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("AssignmentToForLoopParameter")
|
||||||
|
public synchronized void set(final E... elements) {
|
||||||
|
ListIterator<E> oldIt = model.listIterator();
|
||||||
|
for (int from = 0; oldIt.hasNext(); ++from) {
|
||||||
|
int to = Arrays.binarySearch( elements, oldIt.next() );
|
||||||
|
|
||||||
|
if (to != from) {
|
||||||
|
oldIt.remove();
|
||||||
|
fireIntervalRemoved( this, from, from );
|
||||||
|
--from;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int to = 0; to < elements.length; ++to) {
|
||||||
|
E newSite = elements[to];
|
||||||
|
|
||||||
|
if ((to >= model.size()) || !Objects.equals( model.get( to ), newSite )) {
|
||||||
|
model.add( to, newSite );
|
||||||
|
fireIntervalAdded( this, to, to );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((selectedItem == null) || !model.contains( selectedItem ))
|
||||||
|
setSelectedItem( getElementAt( 0 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,24 +18,50 @@
|
|||||||
|
|
||||||
package com.lyndir.masterpassword.gui.util;
|
package com.lyndir.masterpassword.gui.util;
|
||||||
|
|
||||||
import com.lyndir.masterpassword.gui.Res;
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
import java.awt.*;
|
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.annotation.Nullable;
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.border.Border;
|
import javax.swing.border.Border;
|
||||||
import javax.swing.border.CompoundBorder;
|
import javax.swing.border.CompoundBorder;
|
||||||
|
import javax.swing.event.*;
|
||||||
|
import javax.swing.text.DefaultFormatterFactory;
|
||||||
|
import org.jetbrains.annotations.NonNls;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author lhunath, 2014-06-08
|
* @author lhunath, 2014-06-08
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings({ "SerializableStoresNonSerializable", "serial" })
|
||||||
public abstract class Components {
|
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) {
|
public static final float TEXT_SIZE_HEADING = 19f;
|
||||||
GradientPanel container = gradientPanel( null, null );
|
public static final float TEXT_SIZE_CONTROL = 13f;
|
||||||
// container.setBackground( Color.red );
|
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 ) );
|
container.setLayout( new BoxLayout( container, axis ) );
|
||||||
for (final Component component : components)
|
for (final Component component : components)
|
||||||
container.add( component );
|
container.add( component );
|
||||||
@@ -43,31 +69,95 @@ public abstract class Components {
|
|||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GradientPanel borderPanel(final JComponent component, @Nullable final Border border) {
|
public static GradientPanel borderPanel(final int axis, final Component... components) {
|
||||||
return borderPanel( component, border, null );
|
return borderPanel( marginBorder(), null, axis, components );
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GradientPanel borderPanel(final JComponent component, @Nullable final Border border, @Nullable final Color background) {
|
public static GradientPanel borderPanel(@Nullable final Border border, final int axis, final Component... components) {
|
||||||
GradientPanel box = boxLayout( BoxLayout.LINE_AXIS, component );
|
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)
|
if (border != null)
|
||||||
box.setBorder( border );
|
box.setBorder( border );
|
||||||
|
|
||||||
if (background != null)
|
|
||||||
box.setBackground( background );
|
|
||||||
|
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GradientPanel gradientPanel(@Nullable final LayoutManager layout, @Nullable final Color color) {
|
public static GradientPanel panel(@Nullable final LayoutManager layout) {
|
||||||
return new GradientPanel( layout, color ) {
|
return panel( layout, null );
|
||||||
{
|
|
||||||
setOpaque( color != null );
|
|
||||||
setBackground( null );
|
|
||||||
setAlignmentX( LEFT_ALIGNMENT );
|
|
||||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
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.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;
|
||||||
|
|
||||||
|
int option = Arrays.binarySearch( options, selectedValue );
|
||||||
|
return (option < 0)? JOptionPane.CLOSED_OPTION: 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.setLocationByPlatform( true );
|
||||||
|
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.setVisible( true );
|
||||||
|
|
||||||
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static JTextField textField() {
|
public static JTextField textField() {
|
||||||
@@ -75,9 +165,42 @@ public abstract class Components {
|
|||||||
{
|
{
|
||||||
setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
|
setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
|
||||||
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) );
|
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) );
|
||||||
setFont( Res.valueFont().deriveFont( CONTROL_TEXT_SIZE ) );
|
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
|
||||||
setAlignmentX( LEFT_ALIGNMENT );
|
setAlignmentX( LEFT_ALIGNMENT );
|
||||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getMaximumSize() {
|
||||||
|
return new Dimension( Integer.MAX_VALUE, getPreferredSize().height );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JTextField textField(@Nullable final String text, @Nullable final Consumer<String> selection) {
|
||||||
|
return new JTextField( text ) {
|
||||||
|
{
|
||||||
|
setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
|
||||||
|
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) );
|
||||||
|
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
|
||||||
|
setAlignmentX( LEFT_ALIGNMENT );
|
||||||
|
|
||||||
|
if (selection != null)
|
||||||
|
getDocument().addDocumentListener( new DocumentListener() {
|
||||||
|
@Override
|
||||||
|
public void insertUpdate(final DocumentEvent e) {
|
||||||
|
selection.accept( getText() );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeUpdate(final DocumentEvent e) {
|
||||||
|
selection.accept( getText() );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void changedUpdate(final DocumentEvent e) {
|
||||||
|
selection.accept( getText() );
|
||||||
|
}
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -93,7 +216,6 @@ public abstract class Components {
|
|||||||
setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
|
setBorder( BorderFactory.createCompoundBorder( BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
|
||||||
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) );
|
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ) );
|
||||||
setAlignmentX( LEFT_ALIGNMENT );
|
setAlignmentX( LEFT_ALIGNMENT );
|
||||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -103,49 +225,174 @@ public abstract class Components {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static JButton button(final String label) {
|
public static <E> JList<E> list(final ListModel<E> model, final Function<E, String> valueTransformer) {
|
||||||
return new JButton( label ) {
|
return new JList<E>( model ) {
|
||||||
{
|
{
|
||||||
setFont( Res.controlFont().deriveFont( CONTROL_TEXT_SIZE ) );
|
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) {
|
||||||
|
super.getListCellRendererComponent(
|
||||||
|
list, valueTransformer.apply( (E) value ), index, isSelected, cellHasFocus );
|
||||||
|
setBorder( BorderFactory.createEmptyBorder( 2, 4, 2, 4 ) );
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
} );
|
||||||
setAlignmentX( LEFT_ALIGNMENT );
|
setAlignmentX( LEFT_ALIGNMENT );
|
||||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Dimension getMaximumSize() {
|
public Dimension getMaximumSize() {
|
||||||
return new Dimension( 20, getPreferredSize().height );
|
return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE );
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Component stud() {
|
public static JScrollPane scrollPane(final Component child) {
|
||||||
Dimension studDimension = new Dimension( 8, 8 );
|
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 ) {
|
||||||
|
{
|
||||||
|
setFont( Res.fonts().controlFont( TEXT_SIZE_CONTROL ) );
|
||||||
|
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 );
|
Box.Filler rigidArea = new Box.Filler( studDimension, studDimension, studDimension );
|
||||||
rigidArea.setAlignmentX( Component.LEFT_ALIGNMENT );
|
rigidArea.setAlignmentX( Component.LEFT_ALIGNMENT );
|
||||||
rigidArea.setAlignmentY( Component.BOTTOM_ALIGNMENT );
|
|
||||||
rigidArea.setBackground( Color.red );
|
rigidArea.setBackground( Color.red );
|
||||||
return rigidArea;
|
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) {
|
public static JSpinner spinner(final SpinnerModel model) {
|
||||||
return new JSpinner( model ) {
|
return new JSpinner( model ) {
|
||||||
{
|
{
|
||||||
CompoundBorder editorBorder = BorderFactory.createCompoundBorder(
|
CompoundBorder editorBorder = BorderFactory.createCompoundBorder(
|
||||||
BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
|
BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
|
||||||
BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) );
|
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 );
|
((DefaultEditor) getEditor()).getTextField().setBorder( editorBorder );
|
||||||
setAlignmentX( LEFT_ALIGNMENT );
|
setAlignmentX( LEFT_ALIGNMENT );
|
||||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
|
||||||
setBorder( null );
|
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( Res.fonts().controlFont( TEXT_SIZE_HEADING ).deriveFont( Font.BOLD ) );
|
||||||
|
setAlignmentX( LEFT_ALIGNMENT );
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Dimension getMaximumSize() {
|
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) {
|
public static JLabel label(@Nullable final String label) {
|
||||||
return label( label, SwingConstants.LEADING );
|
return label( label, SwingConstants.LEADING );
|
||||||
}
|
}
|
||||||
@@ -162,9 +409,8 @@ public abstract class Components {
|
|||||||
public static JLabel label(@Nullable final String label, final int horizontalAlignment) {
|
public static JLabel label(@Nullable final String label, final int horizontalAlignment) {
|
||||||
return new JLabel( label, horizontalAlignment ) {
|
return new JLabel( label, horizontalAlignment ) {
|
||||||
{
|
{
|
||||||
setFont( Res.controlFont().deriveFont( CONTROL_TEXT_SIZE ) );
|
setFont( Res.fonts().controlFont( TEXT_SIZE_CONTROL ) );
|
||||||
setAlignmentX( LEFT_ALIGNMENT );
|
setAlignmentX( LEFT_ALIGNMENT );
|
||||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -177,30 +423,52 @@ public abstract class Components {
|
|||||||
public static JCheckBox checkBox(final String label) {
|
public static JCheckBox checkBox(final String label) {
|
||||||
return new JCheckBox( label ) {
|
return new JCheckBox( label ) {
|
||||||
{
|
{
|
||||||
setFont( Res.controlFont().deriveFont( CONTROL_TEXT_SIZE ) );
|
setFont( Res.fonts().controlFont( TEXT_SIZE_CONTROL ) );
|
||||||
setBackground( null );
|
setBackground( null );
|
||||||
setAlignmentX( LEFT_ALIGNMENT );
|
setAlignmentX( LEFT_ALIGNMENT );
|
||||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@SafeVarargs
|
@SafeVarargs
|
||||||
public static <V> JComboBox<V> comboBox(final V... values) {
|
public static <E> JComboBox<E> comboBox(final Function<E, String> valueTransformer, final E... values) {
|
||||||
return comboBox( new DefaultComboBoxModel<>( values ) );
|
return comboBox( new DefaultComboBoxModel<>( values ), valueTransformer );
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <M> JComboBox<M> comboBox(final ComboBoxModel<M> model) {
|
public static <E> JComboBox<E> comboBox(final E[] values, final Function<E, String> valueTransformer,
|
||||||
return new JComboBox<M>( model ) {
|
@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
|
||||||
|
return comboBox( CollectionListModel.copy( values ).selection( selectedItem, selectionConsumer ), valueTransformer );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <E> JComboBox<E> comboBox(final Collection<E> values, final Function<E, String> valueTransformer,
|
||||||
|
@Nullable final Consumer<E> selectionConsumer) {
|
||||||
|
return comboBox( CollectionListModel.copy( values ).selection( selectionConsumer ), valueTransformer );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <E> JComboBox<E> comboBox(final Collection<E> values, final Function<E, String> valueTransformer,
|
||||||
|
@Nullable final E selectedItem, @Nullable final Consumer<E> selectionConsumer) {
|
||||||
|
return comboBox( CollectionListModel.copy( values ).selection( selectedItem, selectionConsumer ), valueTransformer );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <E> JComboBox<E> comboBox(final ComboBoxModel<E> model, final Function<E, String> valueTransformer) {
|
||||||
|
return new JComboBox<E>( model ) {
|
||||||
{
|
{
|
||||||
// CompoundBorder editorBorder = BorderFactory.createCompoundBorder(
|
setFont( Res.fonts().valueFont( TEXT_SIZE_CONTROL ) );
|
||||||
// BorderFactory.createLineBorder( Res.colors().controlBorder(), 1, true ),
|
setBorder( BorderFactory.createEmptyBorder( 4, 0, 4, 0 ) );
|
||||||
// BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) );
|
setRenderer( new DefaultListCellRenderer() {
|
||||||
// ((JComponent) ((BasicComboBoxEditor) getEditor()).getEditorComponent()).setBorder(editorBorder);
|
@Override
|
||||||
setFont( Res.controlFont().deriveFont( CONTROL_TEXT_SIZE ) );
|
@SuppressWarnings({ "unchecked", "SerializableStoresNonSerializable" })
|
||||||
|
public Component getListCellRendererComponent(final JList<?> list, final Object value, final int index,
|
||||||
|
final boolean isSelected, final boolean cellHasFocus) {
|
||||||
|
super.getListCellRendererComponent(
|
||||||
|
list, valueTransformer.apply( (E) value ), index, isSelected, cellHasFocus );
|
||||||
|
setBorder( BorderFactory.createEmptyBorder( 0, 4, 0, 4 ) );
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
putClientProperty( "JComboBox.isPopDown", Boolean.TRUE );
|
||||||
setAlignmentX( LEFT_ALIGNMENT );
|
setAlignmentX( LEFT_ALIGNMENT );
|
||||||
setAlignmentY( BOTTOM_ALIGNMENT );
|
|
||||||
// setBorder(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -210,6 +478,24 @@ public abstract class Components {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static JEditorPane linkLabel(@NonNls final String html) {
|
||||||
|
return new JEditorPane( "text/html", "<html><body style='width:640;font-family:sans-serif'>" + html ) {
|
||||||
|
{
|
||||||
|
setOpaque( false );
|
||||||
|
setEditable( false );
|
||||||
|
addHyperlinkListener( event -> {
|
||||||
|
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED)
|
||||||
|
try {
|
||||||
|
Platform.get().open( event.getURL().toURI() );
|
||||||
|
}
|
||||||
|
catch (final URISyntaxException e) {
|
||||||
|
logger.err( e, "After triggering hyperlink: %s", event );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static class GradientPanel extends JPanel {
|
public static class GradientPanel extends JPanel {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -218,10 +504,26 @@ public abstract class Components {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private GradientPaint paint;
|
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 );
|
super( layout );
|
||||||
this.gradientColor = gradientColor;
|
if (getLayout() == null)
|
||||||
|
setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) );
|
||||||
|
|
||||||
|
setGradientColor( gradientColor );
|
||||||
setBackground( null );
|
setBackground( null );
|
||||||
|
setAlignmentX( LEFT_ALIGNMENT );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -231,17 +533,30 @@ public abstract class Components {
|
|||||||
|
|
||||||
public void setGradientColor(@Nullable final Color gradientColor) {
|
public void setGradientColor(@Nullable final Color gradientColor) {
|
||||||
this.gradientColor = gradientColor;
|
this.gradientColor = gradientColor;
|
||||||
revalidate();
|
updatePaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void doLayout() {
|
public void setBackground(@Nullable final Color bg) {
|
||||||
super.doLayout();
|
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
|
@Override
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
//==============================================================================
|
||||||
|
// 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 com.lyndir.masterpassword.gui.SwingExecutorService;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.ref.SoftReference;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import 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> 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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(E selectedItem, @Nullable Consumer<E> selectionConsumer);
|
||||||
|
}
|
||||||
@@ -19,15 +19,21 @@
|
|||||||
package com.lyndir.masterpassword.gui.util;
|
package com.lyndir.masterpassword.gui.util;
|
||||||
|
|
||||||
import com.google.common.primitives.UnsignedInteger;
|
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.*;
|
||||||
|
import javax.swing.event.ChangeListener;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author lhunath, 2016-10-29
|
* @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() {
|
public UnsignedIntegerModel() {
|
||||||
this( UnsignedInteger.ZERO, UnsignedInteger.ZERO, UnsignedInteger.MAX_VALUE, UnsignedInteger.ONE );
|
this( UnsignedInteger.ZERO, UnsignedInteger.ZERO, UnsignedInteger.MAX_VALUE, UnsignedInteger.ONE );
|
||||||
@@ -55,4 +61,79 @@ public class UnsignedIntegerModel extends SpinnerNumberModel {
|
|||||||
public UnsignedInteger getNumber() {
|
public UnsignedInteger getNumber() {
|
||||||
return (UnsignedInteger) super.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(final UnsignedInteger selectedItem, @Nullable final Consumer<UnsignedInteger> selectionConsumer) {
|
||||||
|
if (changeListener != null) {
|
||||||
|
removeChangeListener( changeListener );
|
||||||
|
changeListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue( selectedItem );
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,6 @@
|
|||||||
* @author lhunath, 2018-04-26
|
* @author lhunath, 2018-04-26
|
||||||
*/
|
*/
|
||||||
@ParametersAreNonnullByDefault
|
@ParametersAreNonnullByDefault
|
||||||
package com.lyndir.masterpassword.gui.platform.mac;
|
package com.lyndir.masterpassword.gui.util.platform;
|
||||||
|
|
||||||
import javax.annotation.ParametersAreNonnullByDefault;
|
import javax.annotation.ParametersAreNonnullByDefault;
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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 java.util.Collection;
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
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 =
|
||||||
|
CollectionListModel.<MPUser<?>>copy( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser );
|
||||||
|
private final JComboBox<? extends MPUser<?>> userField =
|
||||||
|
Components.comboBox( usersModel, user -> ifNotNull( user, MPUser::getFullName ) );
|
||||||
|
|
||||||
|
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( userField );
|
||||||
|
|
||||||
|
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() ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
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.MPUser;
|
||||||
|
import com.lyndir.masterpassword.model.impl.MPFileUserManager;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.ComponentAdapter;
|
||||||
|
import java.awt.event.ComponentEvent;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import 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 );
|
||||||
|
|
||||||
|
@SuppressWarnings("FieldCanBeLocal")
|
||||||
|
private final Components.GradientPanel root = Components.borderPanel( Res.colors().frameBg(), BoxLayout.PAGE_AXIS );
|
||||||
|
private final FilesPanel filesPanel = new FilesPanel();
|
||||||
|
private final JPanel userPanel = Components.panel( new BorderLayout( 0, 0 ) );
|
||||||
|
private final UserContentPanel userContent = new UserContentPanel();
|
||||||
|
|
||||||
|
@SuppressWarnings("MagicNumber")
|
||||||
|
public MasterPasswordFrame() {
|
||||||
|
super( "Master Password" );
|
||||||
|
|
||||||
|
setContentPane( root );
|
||||||
|
root.add( filesPanel );
|
||||||
|
root.add( Components.strut() );
|
||||||
|
root.add( userPanel );
|
||||||
|
|
||||||
|
userPanel.add( userContent.getUserToolbar(), BorderLayout.LINE_START );
|
||||||
|
userPanel.add( userContent.getSiteToolbar(), BorderLayout.LINE_END );
|
||||||
|
userPanel.add( Components.borderPanel(
|
||||||
|
BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ),
|
||||||
|
Res.colors().controlBg(), BoxLayout.PAGE_AXIS, userContent ), BorderLayout.CENTER );
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,742 @@
|
|||||||
|
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.MPIncognitoUser;
|
||||||
|
import com.lyndir.masterpassword.gui.model.MPNewSite;
|
||||||
|
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 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;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 Logger logger = Logger.get( UserContentPanel.class );
|
||||||
|
private static final JButton iconButton = Components.button( Res.icons().user(), null, null );
|
||||||
|
|
||||||
|
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 = Components.passwordField();
|
||||||
|
private final JLabel errorLabel = Components.label();
|
||||||
|
private final JLabel identiconLabel = Components.label( SwingConstants.CENTER );
|
||||||
|
|
||||||
|
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 );
|
||||||
|
identiconLabel.setFont( Res.fonts().emoticonsFont( Components.TEXT_SIZE_CONTROL ) );
|
||||||
|
add( Box.createGlue() );
|
||||||
|
|
||||||
|
add( Components.label( "Master Password:" ) );
|
||||||
|
add( Components.strut() );
|
||||||
|
add( masterPasswordField );
|
||||||
|
masterPasswordField.addActionListener( this );
|
||||||
|
masterPasswordField.getDocument().addDocumentListener( this );
|
||||||
|
add( errorLabel );
|
||||||
|
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 {
|
||||||
|
|
||||||
|
public static final int SIZE_RESULT = 48;
|
||||||
|
|
||||||
|
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(), null,
|
||||||
|
"Show site recovery questions." );
|
||||||
|
private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteSite(),
|
||||||
|
"Delete the site from the user." );
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private final MPUser<?> user;
|
||||||
|
private final JLabel passwordLabel = Components.label( SwingConstants.CENTER );
|
||||||
|
private final JLabel passwordField = Components.heading( SwingConstants.CENTER );
|
||||||
|
private final JLabel queryLabel = Components.label();
|
||||||
|
private final JTextField queryField = Components.textField( null, this::updateSites );
|
||||||
|
private final CollectionListModel<MPSite<?>> sitesModel =
|
||||||
|
new CollectionListModel<MPSite<?>>().selection( this::showSiteResult );
|
||||||
|
private final JList<MPSite<?>> sitesList =
|
||||||
|
Components.list( sitesModel, this::getSiteDescription );
|
||||||
|
|
||||||
|
private 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( deleteButton );
|
||||||
|
settingsButton.setEnabled( false );
|
||||||
|
deleteButton.setEnabled( false );
|
||||||
|
|
||||||
|
add( Components.heading( user.getFullName(), SwingConstants.CENTER ) );
|
||||||
|
|
||||||
|
add( passwordLabel );
|
||||||
|
add( passwordField );
|
||||||
|
passwordField.setForeground( Res.colors().highlightFg() );
|
||||||
|
passwordField.setFont( Res.fonts().bigValueFont( SIZE_RESULT ) );
|
||||||
|
add( Box.createGlue() );
|
||||||
|
add( Components.strut() );
|
||||||
|
|
||||||
|
add( queryLabel );
|
||||||
|
queryLabel.setText( strf( "%s's password for:", user.getFullName() ) );
|
||||||
|
add( queryField );
|
||||||
|
queryField.putClientProperty( "JTextField.variant", "search" );
|
||||||
|
queryField.addActionListener( event -> useSite() );
|
||||||
|
queryField.addKeyListener( this );
|
||||||
|
queryField.requestFocusInWindow();
|
||||||
|
add( Components.strut() );
|
||||||
|
add( Components.scrollPane( sitesList ) );
|
||||||
|
sitesModel.registerList( sitesList );
|
||||||
|
add( Box.createGlue() );
|
||||||
|
|
||||||
|
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(), MPResultType::getLongName,
|
||||||
|
site.getResultType(), site::setResultType ),
|
||||||
|
Components.strut() );
|
||||||
|
|
||||||
|
components.add( Components.label( "Login Type:" ),
|
||||||
|
Components.comboBox( MPResultType.values(), MPResultType::getLongName,
|
||||||
|
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, site.getSiteName(), new JOptionPane( Components.panel(
|
||||||
|
BoxLayout.PAGE_AXIS, components.build().toArray( new Component[0] ) ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
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> <Add new site></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() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSiteResult( site, result -> {
|
||||||
|
if (result == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (site instanceof MPFileSite)
|
||||||
|
((MPFileSite) site).use();
|
||||||
|
|
||||||
|
Transferable clipboardContents = new StringSelection( result );
|
||||||
|
Toolkit.getDefaultToolkit().getSystemClipboard().setContents( clipboardContents, null );
|
||||||
|
|
||||||
|
Res.ui( () -> {
|
||||||
|
Window window = SwingUtilities.windowForComponent( UserContentPanel.this );
|
||||||
|
if (window instanceof Frame)
|
||||||
|
((Frame) window).setExtendedState( Frame.ICONIFIED );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSiteResult(@Nullable final MPSite<?> site) {
|
||||||
|
showSiteResult( site, null );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSiteResult(@Nullable final MPSite<?> site, @Nullable final Consumer<String> resultCallback) {
|
||||||
|
if (site == null) {
|
||||||
|
if (resultCallback != null)
|
||||||
|
resultCallback.accept( null );
|
||||||
|
Res.ui( () -> {
|
||||||
|
passwordLabel.setText( " " );
|
||||||
|
passwordField.setText( " " );
|
||||||
|
settingsButton.setEnabled( false );
|
||||||
|
deleteButton.setEnabled( false );
|
||||||
|
} );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Res.job( () -> {
|
||||||
|
try {
|
||||||
|
String result = site.getResult();
|
||||||
|
if (resultCallback != null)
|
||||||
|
resultCallback.accept( result );
|
||||||
|
|
||||||
|
Res.ui( () -> {
|
||||||
|
passwordLabel.setText( strf( "Your password for %s:", site.getSiteName() ) );
|
||||||
|
passwordField.setText( result );
|
||||||
|
settingsButton.setEnabled( true );
|
||||||
|
deleteButton.setEnabled( true );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
|
||||||
|
logger.err( e, "While resolving password for: %s", site );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<>();
|
||||||
|
if (!Strings.isNullOrEmpty( query )) {
|
||||||
|
sites.addAll( new LinkedList<>( user.findSites( 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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -25,7 +25,7 @@ import org.joda.time.format.ISODateTimeFormat;
|
|||||||
/**
|
/**
|
||||||
* @author lhunath, 2016-10-29
|
* @author lhunath, 2016-10-29
|
||||||
*/
|
*/
|
||||||
public final class MPConstants {
|
public final class MPModelConstants {
|
||||||
|
|
||||||
/* Environment */
|
/* Environment */
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ public final class MPConstants {
|
|||||||
* mpw: default path to look for run configuration files if the platform default is not desired.
|
* mpw: default path to look for run configuration files if the platform default is not desired.
|
||||||
*/
|
*/
|
||||||
public static final String env_rcDir = "MPW_RCDIR";
|
public static final String env_rcDir = "MPW_RCDIR";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mpw: permit automatic update checks.
|
* mpw: permit automatic update checks.
|
||||||
*/
|
*/
|
||||||
@@ -33,9 +33,7 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
|
|||||||
// - Meta
|
// - Meta
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
String getName();
|
String getSiteName();
|
||||||
|
|
||||||
void setName(String name);
|
|
||||||
|
|
||||||
// - Algorithm
|
// - Algorithm
|
||||||
|
|
||||||
@@ -59,10 +57,34 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
|
|||||||
|
|
||||||
void setLoginType(@Nullable MPResultType loginType);
|
void setLoginType(@Nullable MPResultType loginType);
|
||||||
|
|
||||||
|
default String getResult()
|
||||||
|
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||||
|
|
||||||
|
return getResult( MPKeyPurpose.Authentication );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
default String getResult(final MPKeyPurpose keyPurpose)
|
||||||
|
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||||
|
return getResult( keyPurpose, null );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
default String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext)
|
||||||
|
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||||
|
return getResult( keyPurpose, keyContext, null );
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state)
|
String getResult(MPKeyPurpose keyPurpose, @Nullable String keyContext, @Nullable String state)
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException;
|
throws MPKeyUnavailableException, MPAlgorithmException;
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
default String getLogin()
|
||||||
|
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||||
|
return getLogin( null );
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
String getLogin(@Nullable String state)
|
String getLogin(@Nullable String state)
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException;
|
throws MPKeyUnavailableException, MPAlgorithmException;
|
||||||
@@ -72,9 +94,9 @@ public interface MPSite<Q extends MPQuestion> extends Comparable<MPSite<?>> {
|
|||||||
@Nonnull
|
@Nonnull
|
||||||
MPUser<?> getUser();
|
MPUser<?> getUser();
|
||||||
|
|
||||||
void addQuestion(Q question);
|
boolean addQuestion(Q question);
|
||||||
|
|
||||||
void deleteQuestion(Q question);
|
boolean deleteQuestion(Q question);
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
Collection<Q> getQuestions();
|
Collection<Q> getQuestions();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
package com.lyndir.masterpassword.model;
|
package com.lyndir.masterpassword.model;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableCollection;
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
@@ -45,6 +46,11 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
|
|||||||
|
|
||||||
void setAlgorithm(MPAlgorithm algorithm);
|
void setAlgorithm(MPAlgorithm algorithm);
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
default MPResultType getDefaultType() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
byte[] getKeyID();
|
byte[] getKeyID();
|
||||||
|
|
||||||
@@ -54,12 +60,11 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
|
|||||||
/**
|
/**
|
||||||
* Performs an authentication attempt against the keyID for this user.
|
* 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.
|
* @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.
|
* @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)
|
void authenticate(char[] masterPassword)
|
||||||
throws MPIncorrectMasterPasswordException, MPAlgorithmException;
|
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.
|
* 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.
|
* @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.
|
* @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)
|
void authenticate(MPMasterKey masterKey)
|
||||||
throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException;
|
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();
|
boolean isMasterKeyAvailable();
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@@ -84,13 +100,29 @@ public interface MPUser<S extends MPSite<?>> extends Comparable<MPUser<?>> {
|
|||||||
|
|
||||||
// - Relations
|
// - Relations
|
||||||
|
|
||||||
void addSite(S site);
|
S addSite(String siteName);
|
||||||
|
|
||||||
void deleteSite(S site);
|
@Nonnull
|
||||||
|
S addSite(S site);
|
||||||
|
|
||||||
|
boolean deleteSite(MPSite<?> site);
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
Collection<S> getSites();
|
Collection<S> getSites();
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
Collection<S> findSites(String query);
|
ImmutableCollection<S> findSites(@Nullable String query);
|
||||||
|
|
||||||
|
boolean addListener(Listener listener);
|
||||||
|
|
||||||
|
boolean removeListener(Listener listener);
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
|
||||||
|
void onUserUpdated(MPUser<?> user);
|
||||||
|
|
||||||
|
void onUserAuthenticated(MPUser<?> user);
|
||||||
|
|
||||||
|
void onUserInvalidated(MPUser<?> user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,21 +11,24 @@ public class Changeable {
|
|||||||
|
|
||||||
private static final ExecutorService changeExecutor = Executors.newSingleThreadExecutor();
|
private static final ExecutorService changeExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
|
private final Object mutex = new Object();
|
||||||
|
private Grouping grouping = Grouping.APPLY;
|
||||||
private boolean changed;
|
private boolean changed;
|
||||||
private boolean batchingChanges;
|
|
||||||
|
|
||||||
void setChanged() {
|
void setChanged() {
|
||||||
synchronized (changeExecutor) {
|
synchronized (mutex) {
|
||||||
if (changed)
|
if (changed)
|
||||||
return;
|
return;
|
||||||
changed = true;
|
|
||||||
|
|
||||||
if (batchingChanges)
|
if (grouping != Grouping.IGNORE)
|
||||||
|
changed = true;
|
||||||
|
if (grouping != Grouping.APPLY)
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
changeExecutor.submit( () -> {
|
changeExecutor.submit( () -> {
|
||||||
synchronized (changeExecutor) {
|
synchronized (changeExecutor) {
|
||||||
if (batchingChanges)
|
if (grouping != Grouping.APPLY)
|
||||||
return;
|
return;
|
||||||
changed = false;
|
changed = false;
|
||||||
}
|
}
|
||||||
@@ -33,20 +36,25 @@ public class Changeable {
|
|||||||
onChanged();
|
onChanged();
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected void onChanged() {
|
protected void onChanged() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void beginChanges() {
|
public void beginChanges() {
|
||||||
synchronized (changeExecutor) {
|
synchronized (mutex) {
|
||||||
batchingChanges = true;
|
grouping = Grouping.BATCH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ignoreChanges() {
|
||||||
|
synchronized (mutex) {
|
||||||
|
grouping = Grouping.IGNORE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean endChanges() {
|
public boolean endChanges() {
|
||||||
synchronized (changeExecutor) {
|
synchronized (mutex) {
|
||||||
batchingChanges = false;
|
grouping = Grouping.APPLY;
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
this.changed = false;
|
this.changed = false;
|
||||||
@@ -56,4 +64,8 @@ public class Changeable {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum Grouping {
|
||||||
|
APPLY, BATCH, IGNORE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,8 +55,10 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setType(final MPResultType type) {
|
public void setType(final MPResultType type) {
|
||||||
this.type = type;
|
if (Objects.equals(this.type, type))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.type = type;
|
||||||
setChanged();
|
setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +72,7 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
|
|||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public abstract MPBasicSite<?> getSite();
|
public abstract MPBasicSite<?, ?> getSite();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onChanged() {
|
protected void onChanged() {
|
||||||
@@ -86,7 +88,7 @@ public abstract class MPBasicQuestion extends Changeable implements MPQuestion {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(final Object obj) {
|
public boolean equals(final Object obj) {
|
||||||
return (this == obj) || ((obj instanceof MPQuestion) && Objects.equals( getKeyword(), ((MPQuestion) obj).getKeyword() ));
|
return this == obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -23,51 +23,47 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
|||||||
|
|
||||||
import com.google.common.primitives.UnsignedInteger;
|
import com.google.common.primitives.UnsignedInteger;
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
import com.lyndir.masterpassword.model.MPQuestion;
|
import com.lyndir.masterpassword.model.*;
|
||||||
import com.lyndir.masterpassword.model.MPSite;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author lhunath, 14-12-16
|
* @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 Collection<Q> questions = new LinkedHashSet<>();
|
||||||
|
private final U user;
|
||||||
|
private final String siteName;
|
||||||
|
|
||||||
private String name;
|
|
||||||
private MPAlgorithm algorithm;
|
private MPAlgorithm algorithm;
|
||||||
private UnsignedInteger counter;
|
private UnsignedInteger counter;
|
||||||
private MPResultType resultType;
|
private MPResultType resultType;
|
||||||
private MPResultType loginType;
|
private MPResultType loginType;
|
||||||
|
|
||||||
private final Collection<Q> questions = new LinkedHashSet<>();
|
protected MPBasicSite(final U user, final String siteName) {
|
||||||
|
this( user, siteName, null, null, null, null );
|
||||||
protected MPBasicSite(final String name, final MPAlgorithm algorithm) {
|
|
||||||
this( name, algorithm, 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) {
|
@Nullable final MPResultType resultType, @Nullable final MPResultType loginType) {
|
||||||
this.name = name;
|
this.user = user;
|
||||||
this.algorithm = algorithm;
|
this.siteName = siteName;
|
||||||
this.counter = (counter == null)? algorithm.mpw_default_counter(): counter;
|
this.algorithm = (algorithm != null)? algorithm: this.user.getAlgorithm();
|
||||||
this.resultType = (resultType == null)? algorithm.mpw_default_result_type(): resultType;
|
this.counter = (counter != null)? counter: this.algorithm.mpw_default_counter();
|
||||||
this.loginType = (loginType == null)? algorithm.mpw_default_login_type(): loginType;
|
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
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getSiteName() {
|
||||||
return name;
|
return siteName;
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setName(final String name) {
|
|
||||||
this.name = name;
|
|
||||||
|
|
||||||
setChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@@ -78,8 +74,10 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAlgorithm(final MPAlgorithm algorithm) {
|
public void setAlgorithm(final MPAlgorithm algorithm) {
|
||||||
this.algorithm = algorithm;
|
if (Objects.equals( this.algorithm, algorithm ))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.algorithm = algorithm;
|
||||||
setChanged();
|
setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,8 +89,10 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setCounter(final UnsignedInteger counter) {
|
public void setCounter(final UnsignedInteger counter) {
|
||||||
this.counter = counter;
|
if (Objects.equals( this.counter, counter ))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.counter = counter;
|
||||||
setChanged();
|
setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,8 +104,10 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setResultType(final MPResultType resultType) {
|
public void setResultType(final MPResultType resultType) {
|
||||||
this.resultType = resultType;
|
if (Objects.equals( this.resultType, resultType ))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.resultType = resultType;
|
||||||
setChanged();
|
setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,8 +119,10 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setLoginType(@Nullable final MPResultType loginType) {
|
public void setLoginType(@Nullable final MPResultType loginType) {
|
||||||
this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() );
|
if (Objects.equals( this.loginType, loginType ))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.loginType = ifNotNullElse( loginType, getAlgorithm().mpw_default_login_type() );
|
||||||
setChanged();
|
setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +139,7 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
|
|||||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||||
|
|
||||||
return getUser().getMasterKey().siteResult(
|
return getUser().getMasterKey().siteResult(
|
||||||
getName(), getAlgorithm(), ifNotNullElse( counter, getAlgorithm().mpw_default_counter() ),
|
getSiteName(), getAlgorithm(), ifNotNullElse( counter, getAlgorithm().mpw_default_counter() ),
|
||||||
keyPurpose, keyContext, type, state );
|
keyPurpose, keyContext, type, state );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +148,7 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
|
|||||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||||
|
|
||||||
return getUser().getMasterKey().siteState(
|
return getUser().getMasterKey().siteState(
|
||||||
getName(), getAlgorithm(), ifNotNullElse( counter, getAlgorithm().mpw_default_counter() ),
|
getSiteName(), getAlgorithm(), ifNotNullElse( counter, getAlgorithm().mpw_default_counter() ),
|
||||||
keyPurpose, keyContext, type, state );
|
keyPurpose, keyContext, type, state );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,17 +161,21 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addQuestion(final Q question) {
|
public boolean addQuestion(final Q question) {
|
||||||
questions.add( question );
|
if (!questions.add( question ))
|
||||||
|
return false;
|
||||||
|
|
||||||
setChanged();
|
setChanged();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteQuestion(final Q question) {
|
public boolean deleteQuestion(final Q question) {
|
||||||
questions.remove( question );
|
if (!questions.remove( question ))
|
||||||
|
return false;
|
||||||
|
|
||||||
setChanged();
|
setChanged();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@@ -178,32 +186,35 @@ public abstract class MPBasicSite<Q extends MPQuestion> extends Changeable imple
|
|||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public abstract MPBasicUser<?> getUser();
|
public U getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onChanged() {
|
protected void onChanged() {
|
||||||
super.onChanged();
|
super.onChanged();
|
||||||
|
|
||||||
getUser().setChanged();
|
if (user instanceof Changeable)
|
||||||
|
((Changeable) user).setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hashCode( getName() );
|
return Objects.hashCode( getSiteName() );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(final Object obj) {
|
public boolean equals(final Object obj) {
|
||||||
return (this == obj) || ((obj instanceof MPSite) && Objects.equals( getName(), ((MPSite<?>) obj).getName() ));
|
return obj == this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(@NotNull final MPSite<?> o) {
|
public int compareTo(@Nonnull final MPSite<?> o) {
|
||||||
return getName().compareTo( o.getName() );
|
return getSiteName().compareTo( o.getSiteName() );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return strf( "{%s: %s}", getClass().getSimpleName(), getName() );
|
return strf( "{%s: %s}", getClass().getSimpleName(), getSiteName() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ package com.lyndir.masterpassword.model.impl;
|
|||||||
|
|
||||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableCollection;
|
||||||
import com.google.common.collect.ImmutableSortedSet;
|
import com.google.common.collect.ImmutableSortedSet;
|
||||||
import com.lyndir.lhunath.opal.system.CodeUtils;
|
import com.lyndir.lhunath.opal.system.CodeUtils;
|
||||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
|
import com.lyndir.masterpassword.model.*;
|
||||||
import com.lyndir.masterpassword.model.MPUser;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
@@ -34,9 +35,11 @@ import javax.annotation.Nullable;
|
|||||||
/**
|
/**
|
||||||
* @author lhunath, 2014-06-08
|
* @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 int avatar;
|
||||||
private final String fullName;
|
private final String fullName;
|
||||||
@@ -44,7 +47,7 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
|
|||||||
@Nullable
|
@Nullable
|
||||||
protected MPMasterKey masterKey;
|
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) {
|
protected MPBasicUser(final String fullName, final MPAlgorithm algorithm) {
|
||||||
this( 0, fullName, algorithm );
|
this( 0, fullName, algorithm );
|
||||||
@@ -63,8 +66,10 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAvatar(final int avatar) {
|
public void setAvatar(final int avatar) {
|
||||||
this.avatar = avatar;
|
if (Objects.equals( this.avatar, avatar ))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.avatar = avatar;
|
||||||
setChanged();
|
setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,8 +87,10 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAlgorithm(final MPAlgorithm algorithm) {
|
public void setAlgorithm(final MPAlgorithm algorithm) {
|
||||||
this.algorithm = algorithm;
|
if (Objects.equals( this.algorithm, algorithm ))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.algorithm = algorithm;
|
||||||
setChanged();
|
setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,12 +98,14 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
|
|||||||
@Override
|
@Override
|
||||||
public byte[] getKeyID() {
|
public byte[] getKeyID() {
|
||||||
try {
|
try {
|
||||||
|
if (isMasterKeyAvailable())
|
||||||
return getMasterKey().getKeyID( getAlgorithm() );
|
return getMasterKey().getKeyID( getAlgorithm() );
|
||||||
}
|
}
|
||||||
catch (final MPException e) {
|
catch (final MPException e) {
|
||||||
logger.wrn( e, "While deriving key ID for user: %s", this );
|
logger.wrn( e, "While deriving key ID for user: %s", this );
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -128,54 +137,97 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
|
|||||||
throw new MPIncorrectMasterPasswordException( this );
|
throw new MPIncorrectMasterPasswordException( this );
|
||||||
|
|
||||||
this.masterKey = masterKey;
|
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
|
@Override
|
||||||
public boolean isMasterKeyAvailable() {
|
public boolean isMasterKeyAvailable() {
|
||||||
return masterKey != null;
|
return (masterKey != null) && masterKey.isValid();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public MPMasterKey getMasterKey()
|
public MPMasterKey getMasterKey()
|
||||||
throws MPKeyUnavailableException {
|
throws MPKeyUnavailableException {
|
||||||
if (masterKey == null)
|
if ((masterKey == null) || !masterKey.isValid())
|
||||||
throw new MPKeyUnavailableException( "Master key was not yet set for: " + this );
|
throw new MPKeyUnavailableException( "Master key was not yet set for: " + this );
|
||||||
|
|
||||||
return masterKey;
|
return masterKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public void addSite(final S site) {
|
public S addSite(final S site) {
|
||||||
sites.add( site );
|
sites.put( site.getSiteName(), site );
|
||||||
|
|
||||||
setChanged();
|
setChanged();
|
||||||
|
return site;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteSite(final S site) {
|
public boolean deleteSite(final MPSite<?> site) {
|
||||||
sites.remove( site );
|
if (!sites.values().remove( site ))
|
||||||
|
return false;
|
||||||
|
|
||||||
setChanged();
|
setChanged();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public Collection<S> getSites() {
|
public Collection<S> getSites() {
|
||||||
return Collections.unmodifiableCollection( sites );
|
return Collections.unmodifiableCollection( sites.values() );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public Collection<S> findSites(final String query) {
|
public ImmutableCollection<S> findSites(@Nullable final String query) {
|
||||||
ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder();
|
ImmutableSortedSet.Builder<S> results = ImmutableSortedSet.naturalOrder();
|
||||||
|
if (query != null)
|
||||||
for (final S site : getSites())
|
for (final S site : getSites())
|
||||||
if (site.getName().startsWith( query ))
|
if (site.getSiteName().startsWith( query ))
|
||||||
results.add( site );
|
results.add( site );
|
||||||
|
|
||||||
return results.build();
|
return results.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean addListener(final Listener listener) {
|
||||||
|
return listeners.add( listener );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean removeListener(final Listener listener) {
|
||||||
|
return listeners.remove( listener );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onChanged() {
|
||||||
|
super.onChanged();
|
||||||
|
|
||||||
|
for (final Listener listener : listeners)
|
||||||
|
listener.onUserUpdated( this );
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hashCode( getFullName() );
|
return Objects.hashCode( getFullName() );
|
||||||
@@ -183,11 +235,11 @@ public abstract class MPBasicUser<S extends MPBasicSite<?>> extends Changeable i
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(final Object obj) {
|
public boolean equals(final Object obj) {
|
||||||
return (this == obj) || ((obj instanceof MPUser) && Objects.equals( getFullName(), ((MPUser<?>) obj).getFullName() ));
|
return this == obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(final MPUser<?> o) {
|
public int compareTo(@Nonnull final MPUser<?> o) {
|
||||||
return getFullName().compareTo( o.getFullName() );
|
return getFullName().compareTo( o.getFullName() );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package com.lyndir.masterpassword.model.impl;
|
|||||||
import com.google.common.primitives.UnsignedInteger;
|
import com.google.common.primitives.UnsignedInteger;
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
import com.lyndir.masterpassword.model.MPSite;
|
import com.lyndir.masterpassword.model.MPSite;
|
||||||
|
import java.util.Objects;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import org.joda.time.Instant;
|
import org.joda.time.Instant;
|
||||||
@@ -31,9 +32,7 @@ import org.joda.time.ReadableInstant;
|
|||||||
* @author lhunath, 14-12-05
|
* @author lhunath, 14-12-05
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("ComparableImplementedButEqualsNotOverridden")
|
@SuppressWarnings("ComparableImplementedButEqualsNotOverridden")
|
||||||
public class MPFileSite extends MPBasicSite<MPFileQuestion> {
|
public class MPFileSite extends MPBasicSite<MPFileUser, MPFileQuestion> {
|
||||||
|
|
||||||
private final MPFileUser user;
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private String url;
|
private String url;
|
||||||
@@ -46,14 +45,8 @@ public class MPFileSite extends MPBasicSite<MPFileQuestion> {
|
|||||||
private String loginState;
|
private String loginState;
|
||||||
|
|
||||||
public MPFileSite(final MPFileUser user, final String name) {
|
public MPFileSite(final MPFileUser user, final String name) {
|
||||||
this( user, name, null, null, null );
|
this( user, name, null, null, null, null, null, null,
|
||||||
}
|
null, 0, new Instant() );
|
||||||
|
|
||||||
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() );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected MPFileSite(final MPFileUser user, final String name,
|
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 resultType, @Nullable final String resultState,
|
||||||
@Nullable final MPResultType loginType, @Nullable final String loginState,
|
@Nullable final MPResultType loginType, @Nullable final String loginState,
|
||||||
@Nullable final String url, final int uses, final ReadableInstant lastUsed) {
|
@Nullable final String url, final int uses, final ReadableInstant lastUsed) {
|
||||||
super( name, (algorithm == null)? user.getAlgorithm(): algorithm, counter,
|
super( user, name, algorithm, counter, resultType, loginType );
|
||||||
(resultType == null)? user.getDefaultType(): resultType, loginType );
|
|
||||||
|
|
||||||
this.user = user;
|
|
||||||
this.resultState = resultState;
|
this.resultState = resultState;
|
||||||
this.loginState = loginState;
|
this.loginState = loginState;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
@@ -77,9 +68,13 @@ public class MPFileSite extends MPBasicSite<MPFileQuestion> {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUrl(@Nullable final String url) {
|
public void setUrl(@Nullable String url) {
|
||||||
this.url = url;
|
if ((url != null) && url.isEmpty())
|
||||||
|
url = null;
|
||||||
|
if (Objects.equals( this.url, url))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.url = url;
|
||||||
setChanged();
|
setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,21 +89,20 @@ public class MPFileSite extends MPBasicSite<MPFileQuestion> {
|
|||||||
public void use() {
|
public void use() {
|
||||||
uses++;
|
uses++;
|
||||||
lastUsed = new Instant();
|
lastUsed = new Instant();
|
||||||
user.use();
|
getUser().use();
|
||||||
}
|
setChanged();
|
||||||
|
|
||||||
public String getResult()
|
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
|
||||||
|
|
||||||
return getResult( MPKeyPurpose.Authentication, null );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext)
|
public String getResult(final MPKeyPurpose keyPurpose, @Nullable final String keyContext)
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||||
|
|
||||||
return getResult( keyPurpose, keyContext, getResultState() );
|
return getResult( keyPurpose, keyContext, getResultState() );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
public String getLogin()
|
public String getLogin()
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
throws MPKeyUnavailableException, MPAlgorithmException {
|
||||||
|
|
||||||
@@ -151,15 +145,9 @@ public class MPFileSite extends MPBasicSite<MPFileQuestion> {
|
|||||||
setChanged();
|
setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
@Override
|
||||||
public MPFileUser getUser() {
|
public int compareTo(@Nonnull final MPSite<?> o) {
|
||||||
return user;
|
int comparison = (o instanceof MPFileSite)? ((MPFileSite) o).getLastUsed().compareTo( getLastUsed() ): 0;
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(final MPSite<?> o) {
|
|
||||||
int comparison = (o instanceof MPFileSite)? -getLastUsed().compareTo( ((MPFileSite) o).getLastUsed() ): 0;
|
|
||||||
if (comparison != 0)
|
if (comparison != 0)
|
||||||
return comparison;
|
return comparison;
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,13 @@
|
|||||||
|
|
||||||
package com.lyndir.masterpassword.model.impl;
|
package com.lyndir.masterpassword.model.impl;
|
||||||
|
|
||||||
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
|
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
|
||||||
import com.lyndir.masterpassword.model.MPUser;
|
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.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import org.joda.time.Instant;
|
import org.joda.time.Instant;
|
||||||
@@ -33,34 +37,46 @@ import org.joda.time.ReadableInstant;
|
|||||||
@SuppressWarnings("ComparableImplementedButEqualsNotOverridden")
|
@SuppressWarnings("ComparableImplementedButEqualsNotOverridden")
|
||||||
public class MPFileUser extends MPBasicUser<MPFileSite> {
|
public class MPFileUser extends MPBasicUser<MPFileSite> {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.get( MPFileUser.class );
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private byte[] keyID;
|
private byte[] keyID;
|
||||||
|
private File path;
|
||||||
private MPMarshalFormat format;
|
private MPMarshalFormat format;
|
||||||
private MPMarshaller.ContentMode contentMode;
|
private MPMarshaller.ContentMode contentMode;
|
||||||
|
|
||||||
private MPResultType defaultType;
|
private MPResultType defaultType;
|
||||||
private ReadableInstant lastUsed;
|
private ReadableInstant lastUsed;
|
||||||
|
private boolean complete;
|
||||||
|
|
||||||
@Nullable
|
@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) {
|
return null;
|
||||||
this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm() );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm) {
|
public MPFileUser(final String fullName, final File path) {
|
||||||
this( fullName, keyID, algorithm, 0, algorithm.mpw_default_result_type(), new Instant(),
|
this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm(), path );
|
||||||
MPMarshalFormat.DEFAULT, MPMarshaller.ContentMode.PROTECTED );
|
}
|
||||||
|
|
||||||
|
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,
|
public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm,
|
||||||
final int avatar, final MPResultType defaultType, final ReadableInstant lastUsed,
|
final int avatar, @Nullable final MPResultType defaultType, final ReadableInstant lastUsed,
|
||||||
final MPMarshalFormat format, final MPMarshaller.ContentMode contentMode) {
|
final MPMarshaller.ContentMode contentMode, final MPMarshalFormat format, final File path) {
|
||||||
super( avatar, fullName, algorithm );
|
super( avatar, fullName, algorithm );
|
||||||
|
|
||||||
this.keyID = (keyID == null)? null: keyID.clone();
|
this.keyID = (keyID != null)? keyID.clone(): null;
|
||||||
this.defaultType = defaultType;
|
this.defaultType = (defaultType != null)? defaultType: algorithm.mpw_default_result_type();
|
||||||
this.lastUsed = lastUsed;
|
this.lastUsed = lastUsed;
|
||||||
|
this.path = path;
|
||||||
this.format = format;
|
this.format = format;
|
||||||
this.contentMode = contentMode;
|
this.contentMode = contentMode;
|
||||||
}
|
}
|
||||||
@@ -71,6 +87,10 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
|
|||||||
return (keyID == null)? null: keyID.clone();
|
return (keyID == null)? null: keyID.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPath(final File path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAlgorithm(final MPAlgorithm algorithm) {
|
public void setAlgorithm(final MPAlgorithm algorithm) {
|
||||||
if (!algorithm.equals( getAlgorithm() ) && (keyID != null)) {
|
if (!algorithm.equals( getAlgorithm() ) && (keyID != null)) {
|
||||||
@@ -96,8 +116,10 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setFormat(final MPMarshalFormat format) {
|
public void setFormat(final MPMarshalFormat format) {
|
||||||
this.format = format;
|
if (Objects.equals( this.format, format ))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.format = format;
|
||||||
setChanged();
|
setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,18 +128,23 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setContentMode(final MPMarshaller.ContentMode contentMode) {
|
public void setContentMode(final MPMarshaller.ContentMode contentMode) {
|
||||||
this.contentMode = contentMode;
|
if (Objects.equals( this.contentMode, contentMode ))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.contentMode = contentMode;
|
||||||
setChanged();
|
setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public MPResultType getDefaultType() {
|
public MPResultType getDefaultType() {
|
||||||
return defaultType;
|
return defaultType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDefaultType(final MPResultType defaultType) {
|
public void setDefaultType(final MPResultType defaultType) {
|
||||||
this.defaultType = defaultType;
|
if (Objects.equals( this.defaultType, defaultType ))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.defaultType = defaultType;
|
||||||
setChanged();
|
setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,17 +154,19 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
|
|||||||
|
|
||||||
public void use() {
|
public void use() {
|
||||||
lastUsed = new Instant();
|
lastUsed = new Instant();
|
||||||
|
|
||||||
setChanged();
|
setChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setJSON(final MPJSONFile json) {
|
protected boolean isComplete() {
|
||||||
this.json = json;
|
return complete;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
protected void setComplete() {
|
||||||
public MPJSONFile getJSON() {
|
complete = true;
|
||||||
return (json == null)? json = new MPJSONFile(): json;
|
}
|
||||||
|
|
||||||
|
public File getFile() {
|
||||||
|
return new File( path, getFullName() + getFormat().fileSuffix() );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -145,33 +174,54 @@ public class MPFileUser extends MPBasicUser<MPFileSite> {
|
|||||||
throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
|
throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
|
||||||
super.authenticate( masterKey );
|
super.authenticate( masterKey );
|
||||||
|
|
||||||
|
try {
|
||||||
|
getFormat().unmarshaller().readSites( this );
|
||||||
|
}
|
||||||
|
catch (final IOException | MPMarshalException e) {
|
||||||
|
logger.err( e, "While reading sites on authentication." );
|
||||||
|
}
|
||||||
|
|
||||||
if (keyID == null) {
|
if (keyID == null) {
|
||||||
keyID = masterKey.getKeyID( getAlgorithm() );
|
keyID = masterKey.getKeyID( getAlgorithm() );
|
||||||
|
|
||||||
setChanged();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MPFileSite addSite(final String siteName) {
|
||||||
|
return addSite( new MPFileSite( this, siteName ) );
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onChanged() {
|
protected void onChanged() {
|
||||||
super.onChanged();
|
|
||||||
|
|
||||||
try {
|
|
||||||
save();
|
save();
|
||||||
}
|
|
||||||
catch (final MPKeyUnavailableException | MPAlgorithmException e) {
|
|
||||||
logger.wrn( e, "Couldn't save change." );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void save()
|
super.onChanged();
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
|
||||||
MPFileUserManager.get().save( this, getMasterKey() );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(final MPUser<?> o) {
|
public int compareTo(@Nonnull final MPUser<?> o) {
|
||||||
int comparison = (o instanceof MPFileUser)? -getLastUsed().compareTo( ((MPFileUser) o).getLastUsed() ): 0;
|
int comparison = (o instanceof MPFileUser)? ((MPFileUser) o).getLastUsed().compareTo( getLastUsed() ): 0;
|
||||||
if (comparison != 0)
|
if (comparison != 0)
|
||||||
return comparison;
|
return comparison;
|
||||||
|
|
||||||
|
|||||||
@@ -20,16 +20,13 @@ package com.lyndir.masterpassword.model.impl;
|
|||||||
|
|
||||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
||||||
|
|
||||||
import com.google.common.base.Charsets;
|
import com.google.common.collect.ImmutableSortedSet;
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import com.google.common.io.CharSink;
|
|
||||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.model.MPModelConstants;
|
||||||
import com.lyndir.masterpassword.model.*;
|
import java.io.File;
|
||||||
import java.io.*;
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.Map;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,20 +35,25 @@ import javax.annotation.Nonnull;
|
|||||||
* @author lhunath, 14-12-07
|
* @author lhunath, 14-12-07
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("CallToSystemGetenv")
|
@SuppressWarnings("CallToSystemGetenv")
|
||||||
public class MPFileUserManager extends MPUserManager<MPFileUser> {
|
public class MPFileUserManager {
|
||||||
|
|
||||||
@SuppressWarnings("UnusedDeclaration")
|
@SuppressWarnings("UnusedDeclaration")
|
||||||
private static final Logger logger = Logger.get( MPFileUserManager.class );
|
private static final Logger logger = Logger.get( MPFileUserManager.class );
|
||||||
private static final MPFileUserManager instance;
|
private static final MPFileUserManager instance;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
String rcDir = System.getenv( MPConstants.env_rcDir );
|
String rcDir = System.getenv( MPModelConstants.env_rcDir );
|
||||||
|
|
||||||
if (rcDir != null)
|
if (rcDir != null)
|
||||||
instance = create( new File( rcDir ) );
|
instance = create( new File( rcDir ) );
|
||||||
else
|
else {
|
||||||
instance = create( new File( ifNotNullElseNullable( System.getProperty( "user.home" ), System.getenv( "HOME" ) ), ".mpw.d" ) );
|
String home = ifNotNullElseNullable( System.getProperty( "user.home" ), System.getenv( "HOME" ) );
|
||||||
|
instance = create( new File( home, ".mpw.d" ) );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final Collection<Listener> listeners = new CopyOnWriteArraySet<>();
|
||||||
|
private final Map<String, MPFileUser> userByName = new HashMap<>();
|
||||||
private final File path;
|
private final File path;
|
||||||
|
|
||||||
public static MPFileUserManager get() {
|
public static MPFileUserManager get() {
|
||||||
@@ -63,86 +65,89 @@ public class MPFileUserManager extends MPUserManager<MPFileUser> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected MPFileUserManager(final File path) {
|
protected MPFileUserManager(final File path) {
|
||||||
|
|
||||||
super( unmarshallUsers( path ) );
|
|
||||||
this.path = path;
|
this.path = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Iterable<MPFileUser> unmarshallUsers(final File userFilesDirectory) {
|
public void reload() {
|
||||||
if (!userFilesDirectory.mkdirs() && !userFilesDirectory.isDirectory()) {
|
userByName.clear();
|
||||||
logger.err( "Couldn't create directory for user files: %s", userFilesDirectory );
|
|
||||||
return ImmutableList.of();
|
File[] pathFiles;
|
||||||
|
if ((!path.exists() && !path.mkdirs()) || ((pathFiles = path.listFiles()) == null)) {
|
||||||
|
logger.err( "Couldn't create directory for user files: %s", path );
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, MPFileUser> users = new HashMap<>();
|
for (final File file : pathFiles)
|
||||||
for (final File userFile : listUserFiles( userFilesDirectory ))
|
|
||||||
for (final MPMarshalFormat format : MPMarshalFormat.values())
|
|
||||||
if (userFile.getName().endsWith( format.fileSuffix() ))
|
|
||||||
try {
|
try {
|
||||||
MPFileUser user = format.unmarshaller().unmarshall( userFile, null );
|
MPFileUser user = MPFileUser.load( file );
|
||||||
MPFileUser previousUser = users.put( user.getFullName(), user );
|
if (user != null) {
|
||||||
|
MPFileUser previousUser = userByName.put( user.getFullName(), user );
|
||||||
if ((previousUser != null) && (previousUser.getFormat().ordinal() > user.getFormat().ordinal()))
|
if ((previousUser != null) && (previousUser.getFormat().ordinal() > user.getFormat().ordinal()))
|
||||||
users.put( previousUser.getFullName(), previousUser );
|
userByName.put( previousUser.getFullName(), previousUser );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (final IOException | MPMarshalException e) {
|
catch (final IOException | MPMarshalException e) {
|
||||||
logger.err( e, "Couldn't read user from: %s", userFile );
|
logger.err( e, "Couldn't read user from: %s", file );
|
||||||
}
|
|
||||||
catch (final MPKeyUnavailableException | MPIncorrectMasterPasswordException | MPAlgorithmException e) {
|
|
||||||
logger.err( e, "Couldn't authenticate user for: %s", userFile );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return users.values();
|
fireUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ImmutableList<File> listUserFiles(final File userFilesDirectory) {
|
public MPFileUser add(final String fullName) {
|
||||||
return ImmutableList.copyOf( ifNotNullElse( userFilesDirectory.listFiles( (dir, name) -> {
|
return add( new MPFileUser( fullName, getPath() ) );
|
||||||
for (final MPMarshalFormat format : MPMarshalFormat.values())
|
|
||||||
if (name.endsWith( format.fileSuffix() ))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} ), new File[0] ) );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public MPFileUser add(final MPFileUser user) {
|
||||||
public void deleteUser(final MPFileUser user) {
|
user.setPath( getPath() );
|
||||||
super.deleteUser( user );
|
user.save();
|
||||||
|
|
||||||
|
MPFileUser oldUser = userByName.put( user.getFullName(), user );
|
||||||
|
if (oldUser != null)
|
||||||
|
oldUser.invalidate();
|
||||||
|
fireUpdated();
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(final MPFileUser user) {
|
||||||
|
user.invalidate();
|
||||||
|
|
||||||
// Remove deleted users.
|
// Remove deleted users.
|
||||||
File userFile = getUserFile( user, user.getFormat() );
|
File userFile = user.getFile();
|
||||||
if (userFile.exists() && !userFile.delete())
|
if (userFile.exists() && !userFile.delete())
|
||||||
logger.err( "Couldn't delete file: %s", userFile );
|
logger.err( "Couldn't delete file: %s", userFile );
|
||||||
|
else if (userByName.values().remove( user ))
|
||||||
|
fireUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Write the current user state to disk.
|
|
||||||
*/
|
|
||||||
public void save(final MPFileUser user, final MPMasterKey masterKey)
|
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
|
||||||
try {
|
|
||||||
MPMarshalFormat format = user.getFormat();
|
|
||||||
new CharSink() {
|
|
||||||
@Override
|
|
||||||
public Writer openStream()
|
|
||||||
throws IOException {
|
|
||||||
return new OutputStreamWriter( new FileOutputStream( getUserFile( user, format ) ), Charsets.UTF_8 );
|
|
||||||
}
|
|
||||||
}.write( format.marshaller().marshall( user ) );
|
|
||||||
}
|
|
||||||
catch (final MPMarshalException | IOException e) {
|
|
||||||
logger.err( e, "Unable to save sites for user: %s", user );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
private File getUserFile(final MPUser<?> user, final MPMarshalFormat format) {
|
|
||||||
return new File( path, user.getFullName() + format.fileSuffix() );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The location on the file system where the user models are stored.
|
|
||||||
*/
|
|
||||||
public File getPath() {
|
public File getPath() {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ImmutableSortedSet<MPFileUser> getFiles() {
|
||||||
|
return ImmutableSortedSet.copyOf( userByName.values() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addListener(final Listener listener) {
|
||||||
|
return listeners.add( listener );
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean removeListener(final Listener listener) {
|
||||||
|
return listeners.remove( listener );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fireUpdated() {
|
||||||
|
if (listeners.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
ImmutableSortedSet<MPFileUser> files = getFiles();
|
||||||
|
|
||||||
|
for (final Listener listener : listeners)
|
||||||
|
listener.onFilesUpdated( files );
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Listener {
|
||||||
|
|
||||||
|
void onFilesUpdated(ImmutableSortedSet<MPFileUser> files);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,12 @@ package com.lyndir.masterpassword.model.impl;
|
|||||||
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
import static com.lyndir.lhunath.opal.system.util.ObjectUtils.*;
|
||||||
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
import static com.lyndir.lhunath.opal.system.util.StringUtils.*;
|
||||||
|
|
||||||
|
import com.google.common.base.Charsets;
|
||||||
|
import com.google.common.io.CharSink;
|
||||||
import com.lyndir.masterpassword.MPAlgorithmException;
|
import com.lyndir.masterpassword.MPAlgorithmException;
|
||||||
import com.lyndir.masterpassword.MPKeyUnavailableException;
|
import com.lyndir.masterpassword.MPKeyUnavailableException;
|
||||||
import com.lyndir.masterpassword.model.MPConstants;
|
import com.lyndir.masterpassword.model.MPModelConstants;
|
||||||
import javax.annotation.Nonnull;
|
import java.io.*;
|
||||||
import org.joda.time.Instant;
|
import org.joda.time.Instant;
|
||||||
|
|
||||||
|
|
||||||
@@ -36,17 +38,19 @@ public class MPFlatMarshaller implements MPMarshaller {
|
|||||||
|
|
||||||
private static final int FORMAT = 1;
|
private static final int FORMAT = 1;
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
@Override
|
||||||
public String marshall(final MPFileUser user)
|
public void marshall(final MPFileUser user)
|
||||||
throws MPKeyUnavailableException, MPMarshalException, MPAlgorithmException {
|
throws IOException, MPKeyUnavailableException, MPMarshalException, MPAlgorithmException {
|
||||||
|
if (!user.isComplete())
|
||||||
|
throw new IllegalStateException( "Cannot marshall an incomplete user: " + user );
|
||||||
|
|
||||||
StringBuilder content = new StringBuilder();
|
StringBuilder content = new StringBuilder();
|
||||||
content.append( "# Master Password site export\n" );
|
content.append( "# Master Password site export\n" );
|
||||||
content.append( "# " ).append( user.getContentMode().description() ).append( '\n' );
|
content.append( "# " ).append( user.getContentMode().description() ).append( '\n' );
|
||||||
content.append( "# \n" );
|
content.append( "# \n" );
|
||||||
content.append( "##\n" );
|
content.append( "##\n" );
|
||||||
content.append( "# Format: " ).append( FORMAT ).append( '\n' );
|
content.append( "# Format: " ).append( FORMAT ).append( '\n' );
|
||||||
content.append( "# Date: " ).append( MPConstants.dateTimeFormatter.print( new Instant() ) ).append( '\n' );
|
content.append( "# Date: " ).append( MPModelConstants.dateTimeFormatter.print( new Instant() ) ).append( '\n' );
|
||||||
content.append( "# User Name: " ).append( user.getFullName() ).append( '\n' );
|
content.append( "# User Name: " ).append( user.getFullName() ).append( '\n' );
|
||||||
content.append( "# Full Name: " ).append( user.getFullName() ).append( '\n' );
|
content.append( "# Full Name: " ).append( user.getFullName() ).append( '\n' );
|
||||||
content.append( "# Avatar: " ).append( user.getAvatar() ).append( '\n' );
|
content.append( "# Avatar: " ).append( user.getAvatar() ).append( '\n' );
|
||||||
@@ -68,18 +72,24 @@ public class MPFlatMarshaller implements MPMarshaller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content.append( strf( "%s %8d %8s %25s\t%25s\t%s\n", //
|
content.append( strf( "%s %8d %8s %25s\t%25s\t%s\n", //
|
||||||
MPConstants.dateTimeFormatter.print( site.getLastUsed() ), // lastUsed
|
MPModelConstants.dateTimeFormatter.print( site.getLastUsed() ), // lastUsed
|
||||||
site.getUses(), // uses
|
site.getUses(), // uses
|
||||||
strf( "%d:%d:%d", //
|
strf( "%d:%d:%d", //
|
||||||
site.getResultType().getType(), // type
|
site.getResultType().getType(), // type
|
||||||
site.getAlgorithm().version().toInt(), // algorithm
|
site.getAlgorithm().version().toInt(), // algorithm
|
||||||
site.getCounter().intValue() ), // counter
|
site.getCounter().intValue() ), // counter
|
||||||
ifNotNullElse( loginName, "" ), // loginName
|
ifNotNullElse( loginName, "" ), // loginName
|
||||||
site.getName(), // siteName
|
site.getSiteName(), // siteName
|
||||||
ifNotNullElse( password, "" ) // password
|
ifNotNullElse( password, "" ) // password
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
|
|
||||||
return content.toString();
|
new CharSink() {
|
||||||
|
@Override
|
||||||
|
public Writer openStream()
|
||||||
|
throws IOException {
|
||||||
|
return new OutputStreamWriter( new FileOutputStream( user.getFile() ), Charsets.UTF_8 );
|
||||||
|
}
|
||||||
|
}.write( content.toString() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,20 +18,19 @@
|
|||||||
|
|
||||||
package com.lyndir.masterpassword.model.impl;
|
package com.lyndir.masterpassword.model.impl;
|
||||||
|
|
||||||
import com.google.common.base.*;
|
import com.google.common.base.Charsets;
|
||||||
import com.google.common.io.CharStreams;
|
import com.google.common.io.CharStreams;
|
||||||
import com.google.common.primitives.UnsignedInteger;
|
import com.google.common.primitives.UnsignedInteger;
|
||||||
import com.lyndir.lhunath.opal.system.CodeUtils;
|
import com.lyndir.lhunath.opal.system.CodeUtils;
|
||||||
import com.lyndir.lhunath.opal.system.logging.Logger;
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
|
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
import com.lyndir.masterpassword.model.MPConstants;
|
import com.lyndir.masterpassword.model.MPModelConstants;
|
||||||
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
|
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import org.joda.time.Instant;
|
import org.joda.time.Instant;
|
||||||
|
|
||||||
|
|
||||||
@@ -49,40 +48,33 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
|
|||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public MPFileUser unmarshall(@Nonnull final File file, @Nullable final char[] masterPassword)
|
public MPFileUser readUser(@Nonnull final File file)
|
||||||
throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
|
throws IOException, MPMarshalException {
|
||||||
try (Reader reader = new InputStreamReader( new FileInputStream( file ), Charsets.UTF_8 )) {
|
try (Reader reader = new InputStreamReader( new FileInputStream( file ), Charsets.UTF_8 )) {
|
||||||
return unmarshall( CharStreams.toString( reader ), masterPassword );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public MPFileUser unmarshall(@Nonnull final String content, @Nullable final char[] masterPassword)
|
|
||||||
throws MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
|
|
||||||
MPFileUser user = null;
|
|
||||||
byte[] keyID = null;
|
byte[] keyID = null;
|
||||||
String fullName = null;
|
String fullName = null;
|
||||||
int mpVersion = 0, importFormat = 0, avatar = 0;
|
int mpVersion = 0, avatar = 0;
|
||||||
boolean clearContent = false, headerStarted = false;
|
boolean clearContent = false, headerStarted = false;
|
||||||
MPResultType defaultType = null;
|
MPResultType defaultType = null;
|
||||||
|
|
||||||
//noinspection HardcodedLineSeparator
|
//noinspection HardcodedLineSeparator
|
||||||
for (final String line : Splitter.on( CharMatcher.anyOf( "\r\n" ) ).omitEmptyStrings().split( content ))
|
for (final String line : CharStreams.readLines( reader ))
|
||||||
// Header delimitor.
|
// Header delimitor.
|
||||||
if (line.startsWith( "##" ))
|
if (line.startsWith( "##" )) {
|
||||||
if (!headerStarted)
|
if (!headerStarted)
|
||||||
// Starts the header.
|
// Starts the header.
|
||||||
headerStarted = true;
|
headerStarted = true;
|
||||||
else
|
else if ((fullName != null) && (keyID != null))
|
||||||
// Ends the header.
|
// Ends the header.
|
||||||
user = new MPFileUser( fullName, keyID, MPAlgorithm.Version.fromInt( mpVersion ).getAlgorithm(),
|
return new MPFileUser( fullName, keyID, MPAlgorithm.Version.fromInt( mpVersion ).getAlgorithm(),
|
||||||
avatar, defaultType, new Instant( 0 ), MPMarshalFormat.Flat,
|
avatar, defaultType, new Instant( 0 ),
|
||||||
clearContent? MPMarshaller.ContentMode.VISIBLE: MPMarshaller.ContentMode.PROTECTED );
|
clearContent? MPMarshaller.ContentMode.VISIBLE: MPMarshaller.ContentMode.PROTECTED,
|
||||||
|
MPMarshalFormat.Flat, file.getParentFile() );
|
||||||
|
}
|
||||||
|
|
||||||
// Comment.
|
// Comment.
|
||||||
else if (line.startsWith( "#" )) {
|
else if (line.startsWith( "#" )) {
|
||||||
if (headerStarted && (user == null)) {
|
if (headerStarted) {
|
||||||
// In header.
|
// In header.
|
||||||
Matcher headerMatcher = headerFormat.matcher( line );
|
Matcher headerMatcher = headerFormat.matcher( line );
|
||||||
if (headerMatcher.matches()) {
|
if (headerMatcher.matches()) {
|
||||||
@@ -93,8 +85,6 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
|
|||||||
keyID = CodeUtils.decodeHex( value );
|
keyID = CodeUtils.decodeHex( value );
|
||||||
else if ("Algorithm".equalsIgnoreCase( name ))
|
else if ("Algorithm".equalsIgnoreCase( name ))
|
||||||
mpVersion = ConversionUtils.toIntegerNN( value );
|
mpVersion = ConversionUtils.toIntegerNN( value );
|
||||||
else if ("Format".equalsIgnoreCase( name ))
|
|
||||||
importFormat = ConversionUtils.toIntegerNN( value );
|
|
||||||
else if ("Avatar".equalsIgnoreCase( name ))
|
else if ("Avatar".equalsIgnoreCase( name ))
|
||||||
avatar = ConversionUtils.toIntegerNN( value );
|
avatar = ConversionUtils.toIntegerNN( value );
|
||||||
else if ("Passwords".equalsIgnoreCase( name ))
|
else if ("Passwords".equalsIgnoreCase( name ))
|
||||||
@@ -105,8 +95,52 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new MPMarshalException( "No full header found in import file." );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readSites(final MPFileUser user)
|
||||||
|
throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
|
||||||
|
user.ignoreChanges();
|
||||||
|
|
||||||
|
if (user.getFile().exists())
|
||||||
|
try (Reader reader = new InputStreamReader( new FileInputStream( user.getFile() ), Charsets.UTF_8 )) {
|
||||||
|
byte[] keyID = null;
|
||||||
|
String fullName = null;
|
||||||
|
int mpVersion = 0, importFormat = 0, avatar = 0;
|
||||||
|
boolean clearContent = false, headerStarted = false, headerEnded = false;
|
||||||
|
MPResultType defaultType = null;
|
||||||
|
|
||||||
|
//noinspection HardcodedLineSeparator
|
||||||
|
for (final String line : CharStreams.readLines( reader ))
|
||||||
|
// Header delimitor.
|
||||||
|
if (line.startsWith( "##" )) {
|
||||||
|
if (!headerStarted)
|
||||||
|
// Starts the header.
|
||||||
|
headerStarted = true;
|
||||||
|
else
|
||||||
|
// Ends the header.
|
||||||
|
headerEnded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment.
|
||||||
|
else if (line.startsWith( "#" )) {
|
||||||
|
if (headerStarted && !headerEnded) {
|
||||||
|
// In header.
|
||||||
|
Matcher headerMatcher = headerFormat.matcher( line );
|
||||||
|
if (headerMatcher.matches()) {
|
||||||
|
String name = headerMatcher.group( 1 ), value = headerMatcher.group( 2 );
|
||||||
|
if ("Format".equalsIgnoreCase( name ))
|
||||||
|
importFormat = ConversionUtils.toIntegerNN( value );
|
||||||
|
else if ("Passwords".equalsIgnoreCase( name ))
|
||||||
|
clearContent = "visible".equalsIgnoreCase( value );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// No comment.
|
// No comment.
|
||||||
else if (user != null) {
|
else if (headerEnded) {
|
||||||
Matcher siteMatcher = unmarshallFormats[importFormat].matcher( line );
|
Matcher siteMatcher = unmarshallFormats[importFormat].matcher( line );
|
||||||
if (!siteMatcher.matches()) {
|
if (!siteMatcher.matches()) {
|
||||||
logger.wrn( "Couldn't parse line: %s, skipping.", line );
|
logger.wrn( "Couldn't parse line: %s, skipping.", line );
|
||||||
@@ -123,7 +157,7 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
|
|||||||
MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ),
|
MPResultType.forType( ConversionUtils.toIntegerNN( siteMatcher.group( 3 ) ) ),
|
||||||
clearContent? null: siteMatcher.group( 6 ),
|
clearContent? null: siteMatcher.group( 6 ),
|
||||||
null, null, null, ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ),
|
null, null, null, ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ),
|
||||||
MPConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() );
|
MPModelConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() );
|
||||||
if (clearContent)
|
if (clearContent)
|
||||||
site.setSitePassword( site.getResultType(), siteMatcher.group( 6 ) );
|
site.setSitePassword( site.getResultType(), siteMatcher.group( 6 ) );
|
||||||
break;
|
break;
|
||||||
@@ -137,7 +171,7 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
|
|||||||
clearContent? null: siteMatcher.group( 8 ),
|
clearContent? null: siteMatcher.group( 8 ),
|
||||||
MPResultType.GeneratedName, clearContent? null: siteMatcher.group( 6 ), null,
|
MPResultType.GeneratedName, clearContent? null: siteMatcher.group( 6 ), null,
|
||||||
ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ),
|
ConversionUtils.toIntegerNN( siteMatcher.group( 2 ) ),
|
||||||
MPConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() );
|
MPModelConstants.dateTimeFormatter.parseDateTime( siteMatcher.group( 1 ) ).toInstant() );
|
||||||
if (clearContent) {
|
if (clearContent) {
|
||||||
site.setSitePassword( site.getResultType(), siteMatcher.group( 8 ) );
|
site.setSitePassword( site.getResultType(), siteMatcher.group( 8 ) );
|
||||||
site.setLoginName( MPResultType.StoredPersonal, siteMatcher.group( 6 ) );
|
site.setLoginName( MPResultType.StoredPersonal, siteMatcher.group( 6 ) );
|
||||||
@@ -153,7 +187,9 @@ public class MPFlatUnmarshaller implements MPUnmarshaller {
|
|||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
throw new MPMarshalException( "No full header found in import file." );
|
throw new MPMarshalException( "No full header found in import file." );
|
||||||
|
}
|
||||||
|
|
||||||
return user;
|
user.setComplete();
|
||||||
|
user.endChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
package com.lyndir.masterpassword.model.impl;
|
package com.lyndir.masterpassword.model.impl;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.*;
|
import com.fasterxml.jackson.annotation.*;
|
||||||
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ class MPJSONAnyObject {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings({ "ChainOfInstanceofChecks", "Contract" })
|
@SuppressWarnings({ "ChainOfInstanceofChecks", "Contract" })
|
||||||
|
@SuppressFBWarnings({ "EQ_UNUSUAL", "EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS", "HE_EQUALS_USE_HASHCODE" })
|
||||||
public boolean equals(final Object obj) {
|
public boolean equals(final Object obj) {
|
||||||
if (obj instanceof Collection<?>)
|
if (obj instanceof Collection<?>)
|
||||||
return ((Collection<?>) obj).isEmpty();
|
return ((Collection<?>) obj).isEmpty();
|
||||||
|
|||||||
@@ -28,9 +28,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.google.common.primitives.UnsignedInteger;
|
import com.google.common.primitives.UnsignedInteger;
|
||||||
import com.lyndir.lhunath.opal.system.CodeUtils;
|
import com.lyndir.lhunath.opal.system.CodeUtils;
|
||||||
import com.lyndir.masterpassword.*;
|
import com.lyndir.masterpassword.*;
|
||||||
import com.lyndir.masterpassword.model.MPConstants;
|
import com.lyndir.masterpassword.model.MPModelConstants;
|
||||||
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
|
import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||||
|
import java.io.File;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
@@ -59,36 +60,37 @@ public class MPJSONFile extends MPJSONAnyObject {
|
|||||||
objectMapper.setVisibility( PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE );
|
objectMapper.setVisibility( PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NON_PRIVATE );
|
||||||
}
|
}
|
||||||
|
|
||||||
public MPJSONFile write(final MPFileUser modelUser)
|
MPJSONFile() {
|
||||||
throws MPKeyUnavailableException, MPAlgorithmException {
|
}
|
||||||
|
|
||||||
|
MPJSONFile(final MPFileUser modelUser)
|
||||||
|
throws MPAlgorithmException, MPKeyUnavailableException {
|
||||||
|
|
||||||
// Section: "export"
|
// Section: "export"
|
||||||
if (export == null)
|
|
||||||
export = new Export();
|
export = new Export();
|
||||||
export.format = 1;
|
export.format = 1;
|
||||||
export.redacted = modelUser.getContentMode().isRedacted();
|
export.redacted = modelUser.getContentMode().isRedacted();
|
||||||
export.date = MPConstants.dateTimeFormatter.print( new Instant() );
|
export.date = MPModelConstants.dateTimeFormatter.print( new Instant() );
|
||||||
|
|
||||||
// Section: "user"
|
// Section: "user"
|
||||||
if (user == null)
|
|
||||||
user = new User();
|
user = new User();
|
||||||
user.avatar = modelUser.getAvatar();
|
user.avatar = modelUser.getAvatar();
|
||||||
user.full_name = modelUser.getFullName();
|
user.full_name = modelUser.getFullName();
|
||||||
user.last_used = MPConstants.dateTimeFormatter.print( modelUser.getLastUsed() );
|
user.last_used = MPModelConstants.dateTimeFormatter.print( modelUser.getLastUsed() );
|
||||||
user.key_id = modelUser.exportKeyID();
|
user.key_id = modelUser.exportKeyID();
|
||||||
user.algorithm = modelUser.getAlgorithm().version();
|
user.algorithm = modelUser.getAlgorithm().version();
|
||||||
user.default_type = modelUser.getDefaultType();
|
user.default_type = modelUser.getDefaultType();
|
||||||
|
|
||||||
// Section "sites"
|
// Section "sites"
|
||||||
if (sites == null)
|
|
||||||
sites = new LinkedHashMap<>();
|
sites = new LinkedHashMap<>();
|
||||||
for (final MPFileSite modelSite : modelUser.getSites()) {
|
for (final MPFileSite modelSite : modelUser.getSites()) {
|
||||||
String content = null, loginContent = null;
|
String content = null, loginContent = null;
|
||||||
|
|
||||||
if (!export.redacted) {
|
if (!export.redacted) {
|
||||||
// Clear Text
|
// Clear Text
|
||||||
content = modelSite.getResult();
|
content = modelSite.getResult();
|
||||||
loginContent = modelUser.getMasterKey().siteResult(
|
loginContent = modelUser.getMasterKey().siteResult(
|
||||||
modelSite.getName(), modelSite.getAlgorithm(), modelSite.getAlgorithm().mpw_default_counter(),
|
modelSite.getSiteName(), modelSite.getAlgorithm(), modelSite.getAlgorithm().mpw_default_counter(),
|
||||||
MPKeyPurpose.Identification, null, modelSite.getLoginType(), modelSite.getLoginState() );
|
MPKeyPurpose.Identification, null, modelSite.getLoginType(), modelSite.getLoginState() );
|
||||||
} else {
|
} else {
|
||||||
// Redacted
|
// Redacted
|
||||||
@@ -98,9 +100,9 @@ public class MPJSONFile extends MPJSONAnyObject {
|
|||||||
loginContent = modelSite.getLoginState();
|
loginContent = modelSite.getLoginState();
|
||||||
}
|
}
|
||||||
|
|
||||||
Site site = sites.get( modelSite.getName() );
|
Site site = sites.get( modelSite.getSiteName() );
|
||||||
if (site == null)
|
if (site == null)
|
||||||
sites.put( modelSite.getName(), site = new Site() );
|
sites.put( modelSite.getSiteName(), site = new Site() );
|
||||||
site.type = modelSite.getResultType();
|
site.type = modelSite.getResultType();
|
||||||
site.counter = modelSite.getCounter().longValue();
|
site.counter = modelSite.getCounter().longValue();
|
||||||
site.algorithm = modelSite.getAlgorithm().version();
|
site.algorithm = modelSite.getAlgorithm().version();
|
||||||
@@ -109,9 +111,8 @@ public class MPJSONFile extends MPJSONAnyObject {
|
|||||||
site.login_type = modelSite.getLoginType();
|
site.login_type = modelSite.getLoginType();
|
||||||
|
|
||||||
site.uses = modelSite.getUses();
|
site.uses = modelSite.getUses();
|
||||||
site.last_used = MPConstants.dateTimeFormatter.print( modelSite.getLastUsed() );
|
site.last_used = MPModelConstants.dateTimeFormatter.print( modelSite.getLastUsed() );
|
||||||
|
|
||||||
if (site.questions == null)
|
|
||||||
site.questions = new LinkedHashMap<>();
|
site.questions = new LinkedHashMap<>();
|
||||||
for (final MPFileQuestion question : modelSite.getQuestions())
|
for (final MPFileQuestion question : modelSite.getQuestions())
|
||||||
site.questions.put( question.getKeyword(), new Site.Question() {
|
site.questions.put( question.getKeyword(), new Site.Question() {
|
||||||
@@ -129,36 +130,34 @@ public class MPJSONFile extends MPJSONAnyObject {
|
|||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
if (site._ext_mpw == null)
|
|
||||||
site._ext_mpw = new Site.Ext();
|
site._ext_mpw = new Site.Ext();
|
||||||
site._ext_mpw.url = modelSite.getUrl();
|
site._ext_mpw.url = modelSite.getUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public MPFileUser read(@Nullable final char[] masterPassword)
|
MPFileUser readUser(final File file) {
|
||||||
throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
|
|
||||||
MPAlgorithm algorithm = ifNotNullElse( user.algorithm, MPAlgorithm.Version.CURRENT ).getAlgorithm();
|
MPAlgorithm algorithm = ifNotNullElse( user.algorithm, MPAlgorithm.Version.CURRENT ).getAlgorithm();
|
||||||
MPFileUser model = new MPFileUser(
|
|
||||||
|
return new MPFileUser(
|
||||||
user.full_name, CodeUtils.decodeHex( user.key_id ), algorithm, user.avatar,
|
user.full_name, CodeUtils.decodeHex( user.key_id ), algorithm, user.avatar,
|
||||||
(user.default_type != null)? user.default_type: algorithm.mpw_default_result_type(),
|
(user.default_type != null)? user.default_type: algorithm.mpw_default_result_type(),
|
||||||
(user.last_used != null)? MPConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(),
|
(user.last_used != null)? MPModelConstants.dateTimeFormatter.parseDateTime( user.last_used ): new Instant(),
|
||||||
MPMarshalFormat.JSON, export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE );
|
export.redacted? MPMarshaller.ContentMode.PROTECTED: MPMarshaller.ContentMode.VISIBLE,
|
||||||
model.beginChanges();
|
MPMarshalFormat.JSON, file.getParentFile()
|
||||||
model.setJSON( this );
|
);
|
||||||
if (masterPassword != null)
|
}
|
||||||
model.authenticate( masterPassword );
|
|
||||||
|
|
||||||
|
void readSites(final MPFileUser user)
|
||||||
|
throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
|
||||||
for (final Map.Entry<String, Site> siteEntry : sites.entrySet()) {
|
for (final Map.Entry<String, Site> siteEntry : sites.entrySet()) {
|
||||||
String siteName = siteEntry.getKey();
|
String siteName = siteEntry.getKey();
|
||||||
Site fileSite = siteEntry.getValue();
|
Site fileSite = siteEntry.getValue();
|
||||||
MPFileSite site = new MPFileSite(
|
MPFileSite site = new MPFileSite(
|
||||||
model, siteName, fileSite.algorithm.getAlgorithm(), UnsignedInteger.valueOf( fileSite.counter ), fileSite.type,
|
user, siteName, fileSite.algorithm.getAlgorithm(), UnsignedInteger.valueOf( fileSite.counter ),
|
||||||
export.redacted? fileSite.password: null,
|
fileSite.type, export.redacted? fileSite.password: null,
|
||||||
fileSite.login_type, export.redacted? fileSite.login_name: null,
|
fileSite.login_type, export.redacted? fileSite.login_name: null,
|
||||||
(fileSite._ext_mpw != null)? fileSite._ext_mpw.url: null, fileSite.uses,
|
(fileSite._ext_mpw != null)? fileSite._ext_mpw.url: null, fileSite.uses,
|
||||||
(fileSite.last_used != null)? MPConstants.dateTimeFormatter.parseDateTime( fileSite.last_used ): new Instant() );
|
(fileSite.last_used != null)? MPModelConstants.dateTimeFormatter.parseDateTime( fileSite.last_used ): new Instant() );
|
||||||
|
|
||||||
if (!export.redacted) {
|
if (!export.redacted) {
|
||||||
if (fileSite.password != null)
|
if (fileSite.password != null)
|
||||||
@@ -168,18 +167,28 @@ public class MPJSONFile extends MPJSONAnyObject {
|
|||||||
fileSite.login_name );
|
fileSite.login_name );
|
||||||
}
|
}
|
||||||
|
|
||||||
model.addSite( site );
|
if (fileSite.questions != null)
|
||||||
}
|
for (final Map.Entry<String, Site.Question> questionEntry : fileSite.questions.entrySet()) {
|
||||||
model.endChanges();
|
Site.Question fileQuestion = questionEntry.getValue();
|
||||||
|
MPFileQuestion question = new MPFileQuestion( site, questionEntry.getKey(),
|
||||||
|
fileQuestion.type, export.redacted? fileQuestion.answer: null );
|
||||||
|
|
||||||
return model;
|
if (!export.redacted && (fileQuestion.answer != null))
|
||||||
|
question.setAnswer( (fileQuestion.type != null)? fileQuestion.type: MPResultType.StoredPersonal,
|
||||||
|
fileQuestion.answer );
|
||||||
|
|
||||||
|
site.addQuestion( question );
|
||||||
|
}
|
||||||
|
|
||||||
|
user.addSite( site );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Data
|
// -- Data
|
||||||
|
|
||||||
Export export;
|
Export export = new Export();
|
||||||
User user;
|
User user = new User();
|
||||||
Map<String, Site> sites;
|
Map<String, Site> sites = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
|
||||||
public static class Export extends MPJSONAnyObject {
|
public static class Export extends MPJSONAnyObject {
|
||||||
@@ -210,7 +219,7 @@ public class MPJSONFile extends MPJSONAnyObject {
|
|||||||
@Nullable
|
@Nullable
|
||||||
MPResultType type;
|
MPResultType type;
|
||||||
long counter;
|
long counter;
|
||||||
MPAlgorithm.Version algorithm;
|
MPAlgorithm.Version algorithm = MPAlgorithm.Version.CURRENT;
|
||||||
@Nullable
|
@Nullable
|
||||||
String password;
|
String password;
|
||||||
@Nullable
|
@Nullable
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import static com.lyndir.masterpassword.model.impl.MPJSONFile.*;
|
|||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.lyndir.masterpassword.MPAlgorithmException;
|
import com.lyndir.masterpassword.MPAlgorithmException;
|
||||||
import com.lyndir.masterpassword.MPKeyUnavailableException;
|
import com.lyndir.masterpassword.MPKeyUnavailableException;
|
||||||
|
import java.io.IOException;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
|
||||||
@@ -33,11 +34,14 @@ public class MPJSONMarshaller implements MPMarshaller {
|
|||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String marshall(final MPFileUser user)
|
public void marshall(final MPFileUser user)
|
||||||
throws MPKeyUnavailableException, MPMarshalException, MPAlgorithmException {
|
throws IOException, MPKeyUnavailableException, MPMarshalException, MPAlgorithmException {
|
||||||
|
|
||||||
|
if (!user.isComplete())
|
||||||
|
throw new IllegalStateException( "Cannot marshall an incomplete user: " + user );
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString( user.getJSON().write( user ) );
|
objectMapper.writerWithDefaultPrettyPrinter().writeValue( user.getFile(), new MPJSONFile( user ) );
|
||||||
}
|
}
|
||||||
catch (final JsonProcessingException e) {
|
catch (final JsonProcessingException e) {
|
||||||
throw new MPMarshalException( "Couldn't compose JSON for: " + user, e );
|
throw new MPMarshalException( "Couldn't compose JSON for: " + user, e );
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,27 +37,11 @@ public class MPJSONUnmarshaller implements MPUnmarshaller {
|
|||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public MPFileUser unmarshall(@Nonnull final File file, @Nullable final char[] masterPassword)
|
public MPFileUser readUser(@Nonnull final File file)
|
||||||
throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
|
throws IOException, MPMarshalException {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return objectMapper.readValue( file, MPJSONFile.class ).read( masterPassword );
|
return objectMapper.readValue( file, MPJSONFile.class ).readUser( file );
|
||||||
}
|
|
||||||
catch (final JsonParseException e) {
|
|
||||||
throw new MPMarshalException( "Couldn't parse JSON in: " + file, e );
|
|
||||||
}
|
|
||||||
catch (final JsonMappingException e) {
|
|
||||||
throw new MPMarshalException( "Couldn't map JSON in: " + file, e );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nonnull
|
|
||||||
@Override
|
|
||||||
public MPFileUser unmarshall(@Nonnull final String content, @Nullable final char[] masterPassword)
|
|
||||||
throws MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
|
|
||||||
|
|
||||||
try {
|
|
||||||
return objectMapper.readValue( content, MPJSONFile.class ).read( masterPassword );
|
|
||||||
}
|
}
|
||||||
catch (final JsonParseException e) {
|
catch (final JsonParseException e) {
|
||||||
throw new MPMarshalException( "Couldn't parse JSON.", e );
|
throw new MPMarshalException( "Couldn't parse JSON.", e );
|
||||||
@@ -66,8 +49,24 @@ public class MPJSONUnmarshaller implements MPUnmarshaller {
|
|||||||
catch (final JsonMappingException e) {
|
catch (final JsonMappingException e) {
|
||||||
throw new MPMarshalException( "Couldn't map JSON.", e );
|
throw new MPMarshalException( "Couldn't map JSON.", e );
|
||||||
}
|
}
|
||||||
catch (final IOException e) {
|
}
|
||||||
throw new MPMarshalException( "Couldn't read JSON.", e );
|
|
||||||
|
@Override
|
||||||
|
public void readSites(final MPFileUser user)
|
||||||
|
throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException {
|
||||||
|
|
||||||
|
try {
|
||||||
|
user.ignoreChanges();
|
||||||
|
if (user.getFile().exists())
|
||||||
|
objectMapper.readValue( user.getFile(), MPJSONFile.class ).readSites( user );
|
||||||
|
user.setComplete();
|
||||||
|
user.endChanges();
|
||||||
|
}
|
||||||
|
catch (final JsonParseException e) {
|
||||||
|
throw new MPMarshalException( "Couldn't parse JSON.", e );
|
||||||
|
}
|
||||||
|
catch (final JsonMappingException e) {
|
||||||
|
throw new MPMarshalException( "Couldn't map JSON.", e );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ package com.lyndir.masterpassword.model.impl;
|
|||||||
|
|
||||||
import com.lyndir.masterpassword.MPAlgorithmException;
|
import com.lyndir.masterpassword.MPAlgorithmException;
|
||||||
import com.lyndir.masterpassword.MPKeyUnavailableException;
|
import com.lyndir.masterpassword.MPKeyUnavailableException;
|
||||||
import javax.annotation.Nonnull;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,9 +29,8 @@ import javax.annotation.Nonnull;
|
|||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface MPMarshaller {
|
public interface MPMarshaller {
|
||||||
|
|
||||||
@Nonnull
|
void marshall(MPFileUser user)
|
||||||
String marshall(MPFileUser user)
|
throws IOException, MPKeyUnavailableException, MPMarshalException, MPAlgorithmException;
|
||||||
throws MPKeyUnavailableException, MPMarshalException, MPAlgorithmException;
|
|
||||||
|
|
||||||
enum ContentMode {
|
enum ContentMode {
|
||||||
PROTECTED( "Export of site names and stored passwords (unless device-private) encrypted with the master key.", true ),
|
PROTECTED( "Export of site names and stored passwords (unless device-private) encrypted with the master key.", true ),
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,10 +32,9 @@ import javax.annotation.Nullable;
|
|||||||
public interface MPUnmarshaller {
|
public interface MPUnmarshaller {
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
MPFileUser unmarshall(@Nonnull File file, @Nullable char[] masterPassword)
|
MPFileUser readUser(File file)
|
||||||
throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException;
|
throws IOException, MPMarshalException;
|
||||||
|
|
||||||
@Nonnull
|
void readSites(MPFileUser user)
|
||||||
MPFileUser unmarshall(@Nonnull String content, @Nullable char[] masterPassword)
|
throws IOException, MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException;
|
||||||
throws MPMarshalException, MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException;
|
|
||||||
}
|
}
|
||||||
|
|||||||