2
0

Compare commits

..

35 Commits

Author SHA1 Message Date
Maarten Billemont
63b4d9cd2e 2.7-java-2 2018-07-31 15:32:13 -04:00
Maarten Billemont
36a7c7f423 Clean up iconifying on copy. 2018-07-31 15:31:47 -04:00
Maarten Billemont
c2c4fb18bf Help improvements. 2018-07-31 15:16:33 -04:00
Maarten Billemont
3fc8acba70 Global hotkey, iconifying and application activation, help text. 2018-07-31 14:55:19 -04:00
Maarten Billemont
f5c0c4d787 Fix offsetting local time back to UTC. 2018-07-31 12:44:49 -04:00
Maarten Billemont
86775f1c75 Standardize epoch time calculation. 2018-07-31 09:27:41 -04:00
Maarten Billemont
2bb190f49a Bump site for masterpassword-gui-2.7.1 release. 2018-07-29 15:38:54 -04:00
Maarten Billemont
77c4a2af46 2.7-java-1 2018-07-29 15:29:19 -04:00
Maarten Billemont
3da82d30b1 Add support for creating incognito users. 2018-07-29 15:26:48 -04:00
Maarten Billemont
97532fdce6 Print UI dates in current time zone. 2018-07-29 15:18:54 -04:00
Maarten Billemont
fe63a2756a Fix default type for new sites & site UI updating. 2018-07-29 15:10:45 -04:00
Maarten Billemont
928b617ed0 Import & export users + improved user state tracking. 2018-07-29 14:01:07 -04:00
Maarten Billemont
18ecc41b39 Fix issues with UnsignedIntegerModel in a spinner. 2018-07-29 01:38:58 -04:00
Maarten Billemont
a6e9e89ace Update UI instead of resetting when site is updated. 2018-07-29 01:38:15 -04:00
Maarten Billemont
0b7494ecbf We use JDK9 APIs now. 2018-07-29 00:19:05 -04:00
Maarten Billemont
8377c9c615 JDK9 platform-independent way of managing application events. 2018-07-29 00:08:09 -04:00
Maarten Billemont
37a7cfa530 Support resetting user's master password. 2018-07-28 21:53:08 -04:00
Maarten Billemont
978b758079 New user fixes. 2018-07-28 19:56:20 -04:00
Maarten Billemont
38f09021b3 Button tooltips and improvements. 2018-07-28 18:11:36 -04:00
Maarten Billemont
7455fba55e Adding and deleting users and sites. 2018-07-28 17:52:43 -04:00
Maarten Billemont
8cd9755616 Update GradientPanel's opaque properly. 2018-07-28 14:30:36 -04:00
Maarten Billemont
46d301df94 Site settings & add sites. 2018-07-28 14:03:49 -04:00
Maarten Billemont
e639137304 Avatar configuration & move preferences into user panel. 2018-07-26 15:07:37 -04:00
Maarten Billemont
7c83a62f91 Support for building with JDK 10. 2018-07-26 15:07:17 -04:00
Maarten Billemont
513840e2c4 Read in site questions from json & don't serialize incomplete MPFileUser 2018-07-23 23:59:11 -04:00
Maarten Billemont
8f7faa9e4e User preferences. 2018-07-23 23:34:32 -04:00
Maarten Billemont
16cdcda94b Identicon support and UI improvements. 2018-07-23 11:23:26 -04:00
Maarten Billemont
400ebe59db Implement sites list and copy result. 2018-07-19 13:56:26 -04:00
Maarten Billemont
476a4046e7 Use standard control highlight color for password. 2018-07-18 17:02:51 -04:00
Maarten Billemont
3403449ca2 Cleanup and fix some warnings. 2018-07-18 15:46:06 -04:00
Maarten Billemont
596ace51ea WIP - new Java UI. 2018-07-18 12:27:19 -04:00
Maarten Billemont
80b5fcd785 Refactor model, improved isolation & access unauthenticated file metadata. 2018-07-18 12:23:53 -04:00
Maarten Billemont
a16bc9a318 Don't save changes made to model while it's being read from file. 2018-07-10 00:54:11 -04:00
Maarten Billemont
462dd4e89b Prepare for 2.7 release. 2018-07-10 00:39:52 -04:00
Maarten Billemont
e5ff374a9c Better way of checking implementation-version. 2018-07-10 00:39:35 -04:00
91 changed files with 3177 additions and 2154 deletions

View File

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

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

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

View File

@@ -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.2'
tasks.withType( JavaCompile ) { tasks.withType( JavaCompile ) {
options.encoding = 'UTF-8' options.encoding = 'UTF-8'

View File

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

View File

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

View File

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

View File

@@ -14,13 +14,12 @@ 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 {
@@ -55,30 +54,32 @@ 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'
} }
} }
} }
}
} }
static String standardOperatingSystem(NativePlatform platform) { static String standardOperatingSystem(NativePlatform platform) {

View File

@@ -1,6 +1,7 @@
#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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,32 +5,37 @@ plugins {
} }
description = 'Master Password GUI' 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
} }

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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 );
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,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
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,6 @@
* @author lhunath, 2018-04-26 * @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;

View File

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

View File

@@ -0,0 +1,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() ) );
}
}

View File

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

View File

@@ -0,0 +1,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();
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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> &lt;Add new site&gt;</html>", queryField.getText() );
ImmutableList.Builder<Object> parameters = ImmutableList.builder();
try {
MPFileSite fileSite = (site instanceof MPFileSite)? (MPFileSite) site: null;
if (fileSite != null)
parameters.add( Res.format( fileSite.getLastUsed() ) );
parameters.add( site.getAlgorithm().version() );
parameters.add( strf( "#%d", site.getCounter().longValue() ) );
parameters.add( strf( "<em>%s</em>", site.getLogin() ) );
if ((fileSite != null) && (fileSite.getUrl() != null))
parameters.add( fileSite.getUrl() );
}
catch (final MPAlgorithmException | MPKeyUnavailableException e) {
logger.err( e, "While generating site description." );
parameters.add( e.getLocalizedMessage() );
}
return strf( "<html><strong>%s</strong> (%s)</html>", site.getSiteName(),
Joiner.on( " - " ).skipNulls().join( parameters.build() ) );
}
private void useSite() {
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) {
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -25,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.
*/ */

View File

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

View File

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

View File

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

View File

@@ -11,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
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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