Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c928b1ca2c | ||
|
|
553a14dced | ||
|
|
8b8d727ee0 | ||
|
|
4cdeab4256 | ||
|
|
bc3aa3255e | ||
|
|
f2fdca6a03 | ||
|
|
647235616e | ||
|
|
b0b6dcc56b | ||
|
|
918a240dba | ||
|
|
830dcb45ff | ||
|
|
7be9884075 | ||
|
|
5ca0d954bb | ||
|
|
02d69261df | ||
|
|
fc60460935 | ||
|
|
559e11b16e | ||
|
|
0a72809b02 | ||
|
|
8c71ed0081 | ||
|
|
3e19a026ba | ||
|
|
8fa3c6c75d | ||
|
|
217cf56d94 | ||
|
|
6f37f28a4c | ||
|
|
3b7d2dc08e | ||
|
|
5e9af44736 | ||
|
|
be33a29fa0 | ||
|
|
29ed22d0b7 | ||
|
|
8b997528c9 | ||
|
|
04a6c8e68d | ||
|
|
0e8e4dc06d | ||
|
|
8b91c2a0b8 | ||
|
|
e967affddb | ||
|
|
dea7434bd4 | ||
|
|
1da63e450d | ||
|
|
029d240999 | ||
|
|
110f7069e1 | ||
|
|
d77cde1929 | ||
|
|
2dba4c87ba | ||
|
|
6841ae2b0d | ||
|
|
a3698b9e47 | ||
|
|
5b65c8c6bd | ||
|
|
94c9d50a12 | ||
|
|
5849c9668f | ||
|
|
4d4ba3425e | ||
|
|
b7e91358be | ||
|
|
da860d74c4 | ||
|
|
4e2ceb33a0 | ||
|
|
d14bde07bd | ||
|
|
fa2dc822d3 | ||
|
|
d4adafb448 | ||
|
|
a67d9676ba | ||
|
|
bc2da6a99b | ||
|
|
50b5c87f61 | ||
|
|
d4bcad2658 | ||
|
|
13bca309ba | ||
|
|
83bdb9d626 | ||
|
|
5f1fc453d4 | ||
|
|
9c875d4311 | ||
|
|
afafa473a7 | ||
|
|
4d5f609d72 | ||
|
|
6f1d53ea35 | ||
|
|
a8bf74a925 | ||
|
|
d59f77720c | ||
|
|
92be7f7267 | ||
|
|
09d5e64c55 | ||
|
|
f796888901 | ||
|
|
679990dc4b | ||
|
|
b472c85c9d | ||
|
|
77306e0046 | ||
|
|
0491ba3f97 | ||
|
|
ba299d4674 | ||
|
|
3de9a0c67e | ||
|
|
7d0ea4b3f5 | ||
|
|
ac14b10752 | ||
|
|
2d8f2943e5 | ||
|
|
4c0348b5c8 | ||
|
|
a5d2f82db4 | ||
|
|
ec8eff2117 |
8
.gitignore
vendored
@@ -20,6 +20,14 @@
|
|||||||
!/*.xcodeproj/project.xcworkspace/*
|
!/*.xcodeproj/project.xcworkspace/*
|
||||||
/*.xcodeproj/project.xcworkspace/xcuserdata
|
/*.xcodeproj/project.xcworkspace/xcuserdata
|
||||||
|
|
||||||
|
# Media
|
||||||
|
Press/Background.png
|
||||||
|
Press/Front-Page.png
|
||||||
|
Press/MasterPassword_PressKit/MasterPassword_pressrelease_*.pdf
|
||||||
|
|
||||||
# IPA
|
# IPA
|
||||||
/sendipa/*
|
/sendipa/*
|
||||||
!/sendipa/sendipa.conf
|
!/sendipa/sendipa.conf
|
||||||
|
|
||||||
|
# Java
|
||||||
|
MasterPassword/Java/**/target
|
||||||
|
|||||||
5
.gitmodules
vendored
@@ -6,4 +6,7 @@
|
|||||||
url = git://github.com/futuretap/InAppSettingsKit.git
|
url = git://github.com/futuretap/InAppSettingsKit.git
|
||||||
[submodule "External/iCloudStoreManager"]
|
[submodule "External/iCloudStoreManager"]
|
||||||
path = External/iCloudStoreManager
|
path = External/iCloudStoreManager
|
||||||
url = git://github.com/alekseyn/iCloudStoreManager.git
|
url = git://github.com/lhunath/iCloudStoreManager.git
|
||||||
|
[submodule "External/FontReplacer"]
|
||||||
|
path = External/FontReplacer
|
||||||
|
url = git://github.com/0xced/FontReplacer.git
|
||||||
|
|||||||
11
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0" is_locked="false">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<option name="myLocal" value="false" />
|
||||||
|
<inspection_tool class="FunctionImplicitDeclarationInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="LossyEncoding" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="MethodIsLaterInTheScope" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="OCUnusedMethodInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="UnusedLocalVariable" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
7
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="PROJECT_PROFILE" value="Project Default" />
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="true" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
@@ -7,6 +7,46 @@
|
|||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* The CLS_LOG macro provides as easy way to gather more information in your log messages that are
|
||||||
|
* sent with your crash data. CLS_LOG prepends your custom log message with the function name and
|
||||||
|
* line number where the macro was used. If your app was built with the DEBUG preprocessor macro
|
||||||
|
* defined CLS_LOG uses the CLSNSLog function which forwards your log message to NSLog and CLSLog.
|
||||||
|
* If the DEBUG preprocessor macro is not defined CLS_LOG uses CLSLog only.
|
||||||
|
*
|
||||||
|
* Example output:
|
||||||
|
* -[AppDelegate login:] line 134 $ login start
|
||||||
|
*
|
||||||
|
* If you would like to change this macro, create a new header file, unset our define and then define
|
||||||
|
* your own version. Make sure this new header file is imported after the Crashlytics header file.
|
||||||
|
*
|
||||||
|
* #undef CLS_LOG
|
||||||
|
* #define CLS_LOG(__FORMAT__, ...) CLSNSLog...
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
#ifdef DEBUG
|
||||||
|
#define CLS_LOG(__FORMAT__, ...) CLSNSLog((@"%s line %d $ " __FORMAT__), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||||
|
#else
|
||||||
|
#define CLS_LOG(__FORMAT__, ...) CLSLog((@"%s line %d $ " __FORMAT__), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Add logging that will be sent with your crash data. This logging will not show up in the system.log
|
||||||
|
* and will only be visible in your Crashlytics dashboard.
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
void CLSLog(NSString *format, ...);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Add logging that will be sent with your crash data. This logging will show up in the system.log
|
||||||
|
* and your Crashlytics dashboard. It is not reccomended for Release builds.
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
void CLSNSLog(NSString *format, ...);
|
||||||
|
|
||||||
@protocol CrashlyticsDelegate;
|
@protocol CrashlyticsDelegate;
|
||||||
|
|
||||||
@interface Crashlytics : NSObject {
|
@interface Crashlytics : NSObject {
|
||||||
@@ -63,6 +103,49 @@
|
|||||||
**/
|
**/
|
||||||
- (void)crash;
|
- (void)crash;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Many of our customers have requested the ability to tie crashes to specific end-users of their
|
||||||
|
* application in order to facilitate responses to support requests or permit the ability to reach
|
||||||
|
* out for more information. We allow you to specify up to three separate values for display within
|
||||||
|
* the Crashlytics UI - but please be mindful of your end-user's privacy.
|
||||||
|
*
|
||||||
|
* We recommend specifying a user identifier - an arbitrary string that ties an end-user to a record
|
||||||
|
* in your system. This could be a database id, hash, or other value that is meaningless to a
|
||||||
|
* third-party observer but can be indexed and queried by you.
|
||||||
|
*
|
||||||
|
* Optionally, you may also specify the end-user's name or username, as well as email address if you
|
||||||
|
* do not have a system that works well with obscured identifiers.
|
||||||
|
*
|
||||||
|
* Pursuant to our EULA, this data is transferred securely throughout our system and we will not
|
||||||
|
* disseminate end-user data unless required to by law. That said, if you choose to provide end-user
|
||||||
|
* contact information, we strongly recommend that you disclose this in your application's privacy
|
||||||
|
* policy. Data privacy is of our utmost concern.
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
- (void)setUserIdentifier:(NSString *)identifier;
|
||||||
|
- (void)setUserName:(NSString *)name;
|
||||||
|
- (void)setUserEmail:(NSString *)email;
|
||||||
|
|
||||||
|
+ (void)setUserIdentifier:(NSString *)identifier;
|
||||||
|
+ (void)setUserName:(NSString *)name;
|
||||||
|
+ (void)setUserEmail:(NSString *)email;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Set a value for a key to be associated with your crash data.
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
- (void)setObjectValue:(id)value forKey:(NSString *)key;
|
||||||
|
- (void)setIntValue:(int)value forKey:(NSString *)key;
|
||||||
|
- (void)setBoolValue:(BOOL)value forKey:(NSString *)key;
|
||||||
|
- (void)setFloatValue:(float)value forKey:(NSString *)key;
|
||||||
|
|
||||||
|
+ (void)setObjectValue:(id)value forKey:(NSString *)key;
|
||||||
|
+ (void)setIntValue:(int)value forKey:(NSString *)key;
|
||||||
|
+ (void)setBoolValue:(BOOL)value forKey:(NSString *)key;
|
||||||
|
+ (void)setFloatValue:(float)value forKey:(NSString *)key;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,7 +163,7 @@
|
|||||||
*
|
*
|
||||||
* Called once a Crashlytics instance has determined that the last execution of the
|
* Called once a Crashlytics instance has determined that the last execution of the
|
||||||
* application ended in a crash. This is called some time after the crash reporting
|
* application ended in a crash. This is called some time after the crash reporting
|
||||||
* process has begun. If you have specififed a delay in one of the
|
* process has begun. If you have specified a delay in one of the
|
||||||
* startWithAPIKey:... calls, this will take at least that long to be invoked.
|
* startWithAPIKey:... calls, this will take at least that long to be invoked.
|
||||||
*
|
*
|
||||||
**/
|
**/
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>BuildMachineOSBuild</key>
|
<key>BuildMachineOSBuild</key>
|
||||||
<string>11C74</string>
|
<string>11E53</string>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>English</string>
|
<string>English</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>FMWK</string>
|
<string>FMWK</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.1.3</string>
|
<string>1.1.5</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleSupportedPlatforms</key>
|
<key>CFBundleSupportedPlatforms</key>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<string>iPhoneOS</string>
|
<string>iPhoneOS</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>0100.01.03</string>
|
<string>0101.05.00</string>
|
||||||
<key>CrashlyticsAPIKey</key>
|
<key>CrashlyticsAPIKey</key>
|
||||||
<string>0d10c90776f5ef5acd01ddbeaca9a6cba4814560</string>
|
<string>0d10c90776f5ef5acd01ddbeaca9a6cba4814560</string>
|
||||||
<key>DTCompiler</key>
|
<key>DTCompiler</key>
|
||||||
|
|||||||
BIN
Crashlytics/Crashlytics.framework/run
vendored
1
External/FontReplacer
vendored
Submodule
2
External/Pearl
vendored
2
External/iCloudStoreManager
vendored
@@ -63,14 +63,23 @@
|
|||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
savedToolIdentifier = ""
|
savedToolIdentifier = ""
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
buildConfiguration = "Production"
|
buildConfiguration = "AppStore"
|
||||||
debugDocumentVersioning = "YES">
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||||
|
BuildableName = "MasterPassword.app"
|
||||||
|
BlueprintName = "MasterPassword"
|
||||||
|
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
</ProfileAction>
|
</ProfileAction>
|
||||||
<AnalyzeAction
|
<AnalyzeAction
|
||||||
buildConfiguration = "Debug">
|
buildConfiguration = "Debug">
|
||||||
</AnalyzeAction>
|
</AnalyzeAction>
|
||||||
<ArchiveAction
|
<ArchiveAction
|
||||||
buildConfiguration = "Production"
|
buildConfiguration = "AppStore"
|
||||||
revealArchiveInOrganizer = "YES">
|
revealArchiveInOrganizer = "YES">
|
||||||
</ArchiveAction>
|
</ArchiveAction>
|
||||||
</Scheme>
|
</Scheme>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES">
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "NO"
|
buildForTesting = "YES"
|
||||||
buildForRunning = "YES"
|
buildForRunning = "YES"
|
||||||
buildForProfiling = "YES"
|
buildForProfiling = "YES"
|
||||||
buildForArchiving = "YES"
|
buildForArchiving = "YES"
|
||||||
@@ -28,6 +28,16 @@
|
|||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
buildConfiguration = "Debug">
|
buildConfiguration = "Debug">
|
||||||
<Testables>
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "DA3EF17815A47744003ABF4E"
|
||||||
|
BuildableName = "Tests.octest"
|
||||||
|
BlueprintName = "Tests"
|
||||||
|
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
</Testables>
|
</Testables>
|
||||||
<MacroExpansion>
|
<MacroExpansion>
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
@@ -40,8 +50,8 @@
|
|||||||
</MacroExpansion>
|
</MacroExpansion>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.GDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.GDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
@@ -57,12 +67,6 @@
|
|||||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
<CommandLineArguments>
|
|
||||||
<CommandLineArgument
|
|
||||||
argument = "-com.apple.coredata.ubiquity.logLevel 3"
|
|
||||||
isEnabled = "NO">
|
|
||||||
</CommandLineArgument>
|
|
||||||
</CommandLineArguments>
|
|
||||||
<AdditionalOptions>
|
<AdditionalOptions>
|
||||||
<AdditionalOption
|
<AdditionalOption
|
||||||
key = "NSZombieEnabled"
|
key = "NSZombieEnabled"
|
||||||
@@ -75,7 +79,7 @@
|
|||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
savedToolIdentifier = ""
|
savedToolIdentifier = ""
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
buildConfiguration = "AdHoc"
|
buildConfiguration = "Debug"
|
||||||
debugDocumentVersioning = "YES">
|
debugDocumentVersioning = "YES">
|
||||||
<BuildableProductRunnable>
|
<BuildableProductRunnable>
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
|
|||||||
BIN
MasterPassword.sketch/QuickLook/Preview.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 43 KiB |
BIN
MasterPassword.sketch/QuickLook/Thumbnail.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
49
MasterPassword/Java/masterpassword-algorithm/pom.xml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<!-- PROJECT METADATA -->
|
||||||
|
<parent>
|
||||||
|
<groupId>com.lyndir.lhunath.masterpassword</groupId>
|
||||||
|
<artifactId>masterpassword</artifactId>
|
||||||
|
<version>GIT-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<name>Master Password Algorithm Implementation</name>
|
||||||
|
<description>The implementation of the Master Password algorithm</description>
|
||||||
|
|
||||||
|
<groupId>com.lyndir.lhunath.masterpassword</groupId>
|
||||||
|
<artifactId>masterpassword-algorithm</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<!-- DEPENDENCY MANAGEMENT -->
|
||||||
|
<dependencies>
|
||||||
|
|
||||||
|
<!-- PROJECT REFERENCES -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.lyndir.lhunath.opal</groupId>
|
||||||
|
<artifactId>opal-system</artifactId>
|
||||||
|
<version>GIT-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.lyndir.lhunath.opal</groupId>
|
||||||
|
<artifactId>opal-crypto</artifactId>
|
||||||
|
<version>GIT-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- EXTERNAL DEPENDENCIES -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.sf.plist</groupId>
|
||||||
|
<artifactId>property-list</artifactId>
|
||||||
|
<version>svn-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.lambdaworks</groupId>
|
||||||
|
<artifactId>scrypt</artifactId>
|
||||||
|
<version>1.3.2</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.lyndir.lhunath.masterpassword;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <i>07 04, 2012</i>
|
||||||
|
*
|
||||||
|
* @author lhunath
|
||||||
|
*/
|
||||||
|
public enum MPElementFeature {
|
||||||
|
|
||||||
|
/** Export the key-protected content data. */
|
||||||
|
ExportContent,
|
||||||
|
/** Never export content. */
|
||||||
|
DevicePrivate,
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.lyndir.lhunath.masterpassword;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <i>07 04, 2012</i>
|
||||||
|
*
|
||||||
|
* @author lhunath
|
||||||
|
*/
|
||||||
|
public enum MPElementType {
|
||||||
|
|
||||||
|
GeneratedMaximum( "Maximum Security Password", "Maximum", "20 characters, contains symbols.", MPElementTypeClass.Generated ),
|
||||||
|
GeneratedLong( "Long Password", "Long", "Copy-friendly, 14 characters, contains symbols.", MPElementTypeClass.Generated ),
|
||||||
|
GeneratedMedium( "Medium Password", "Medium", "Copy-friendly, 8 characters, contains symbols.", MPElementTypeClass.Generated ),
|
||||||
|
GeneratedShort( "Short Password", "Short", "Copy-friendly, 4 characters, no symbols.", MPElementTypeClass.Generated ),
|
||||||
|
GeneratedBasic( "Basic Password", "Basic", "8 characters, no symbols.", MPElementTypeClass.Generated ),
|
||||||
|
GeneratedPIN( "PIN", "PIN", "4 numbers.", MPElementTypeClass.Generated ),
|
||||||
|
|
||||||
|
StoredPersonal( "Personal Password", "Personal", "AES-encrypted, exportable.", MPElementTypeClass.Stored, MPElementFeature.ExportContent ),
|
||||||
|
StoredDevicePrivate( "Device Private Password", "Private", "AES-encrypted, not exported.", MPElementTypeClass.Stored, MPElementFeature.DevicePrivate );
|
||||||
|
|
||||||
|
static final Logger logger = Logger.get( MPElementType.class );
|
||||||
|
|
||||||
|
private final MPElementTypeClass typeClass;
|
||||||
|
private final Set<MPElementFeature> typeFeatures;
|
||||||
|
private final String name;
|
||||||
|
private final String shortName;
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
MPElementType(final String name, final String shortName, final String description, final MPElementTypeClass typeClass, final MPElementFeature... typeFeatures) {
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
this.shortName = shortName;
|
||||||
|
this.typeClass = typeClass;
|
||||||
|
this.description = description;
|
||||||
|
|
||||||
|
ImmutableSet.Builder<MPElementFeature> typeFeaturesBuilder = ImmutableSet.builder();
|
||||||
|
for (final MPElementFeature typeFeature : typeFeatures)
|
||||||
|
typeFeaturesBuilder.add( typeFeature );
|
||||||
|
this.typeFeatures = typeFeaturesBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MPElementTypeClass getTypeClass() {
|
||||||
|
|
||||||
|
return typeClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<MPElementFeature> getTypeFeatures() {
|
||||||
|
|
||||||
|
return typeFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getShortName() {
|
||||||
|
|
||||||
|
return shortName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MPElementType forName(final String name) {
|
||||||
|
|
||||||
|
for (final MPElementType type : values())
|
||||||
|
if (type.getName().equals( name ))
|
||||||
|
return type;
|
||||||
|
|
||||||
|
throw logger.bug( "Element type not known: %s", name );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.lyndir.lhunath.masterpassword;
|
||||||
|
|
||||||
|
import com.lyndir.lhunath.masterpassword.entity.*;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <i>07 04, 2012</i>
|
||||||
|
*
|
||||||
|
* @author lhunath
|
||||||
|
*/
|
||||||
|
public enum MPElementTypeClass {
|
||||||
|
|
||||||
|
Generated(MPElementGeneratedEntity.class),
|
||||||
|
Stored(MPElementStoredEntity.class);
|
||||||
|
|
||||||
|
private final Class<? extends MPElementEntity> entityClass;
|
||||||
|
|
||||||
|
MPElementTypeClass(final Class<? extends MPElementEntity> entityClass) {
|
||||||
|
|
||||||
|
this.entityClass = entityClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Class<? extends MPElementEntity> getEntityClass() {
|
||||||
|
|
||||||
|
return entityClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.lyndir.lhunath.masterpassword;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.lyndir.lhunath.opal.system.util.MetaObject;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <i>07 04, 2012</i>
|
||||||
|
*
|
||||||
|
* @author lhunath
|
||||||
|
*/
|
||||||
|
public class MPTemplate extends MetaObject {
|
||||||
|
|
||||||
|
private final List<MPTemplateCharacterClass> template;
|
||||||
|
|
||||||
|
public MPTemplate(final String template, final Map<Character, MPTemplateCharacterClass> characterClasses) {
|
||||||
|
|
||||||
|
ImmutableList.Builder<MPTemplateCharacterClass> builder = ImmutableList.<MPTemplateCharacterClass>builder();
|
||||||
|
for (int i = 0; i < template.length(); ++i)
|
||||||
|
builder.add( characterClasses.get( template.charAt( i ) ) );
|
||||||
|
|
||||||
|
this.template = builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MPTemplate(final List<MPTemplateCharacterClass> template) {
|
||||||
|
|
||||||
|
this.template = template;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MPTemplateCharacterClass getCharacterClassAtIndex(final int index) {
|
||||||
|
|
||||||
|
return template.get( index );
|
||||||
|
}
|
||||||
|
|
||||||
|
public int length() {
|
||||||
|
|
||||||
|
return template.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.lyndir.lhunath.masterpassword;
|
||||||
|
|
||||||
|
import com.lyndir.lhunath.opal.system.util.MetaObject;
|
||||||
|
import com.lyndir.lhunath.opal.system.util.ObjectMeta;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <i>07 04, 2012</i>
|
||||||
|
*
|
||||||
|
* @author lhunath
|
||||||
|
*/
|
||||||
|
public class MPTemplateCharacterClass extends MetaObject {
|
||||||
|
|
||||||
|
private final char identifier;
|
||||||
|
@ObjectMeta(useFor = { })
|
||||||
|
private final char[] characters;
|
||||||
|
|
||||||
|
public MPTemplateCharacterClass(final char identifier, final char[] characters) {
|
||||||
|
|
||||||
|
this.identifier = identifier;
|
||||||
|
this.characters = characters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public char getIdentifier() {
|
||||||
|
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public char getCharacterAtRollingIndex(final int index) {
|
||||||
|
|
||||||
|
return characters[index % characters.length];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package com.lyndir.lhunath.masterpassword;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.base.Throwables;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.io.Closeables;
|
||||||
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
|
import com.lyndir.lhunath.opal.system.util.MetaObject;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import net.sf.plist.*;
|
||||||
|
import net.sf.plist.io.PropertyListException;
|
||||||
|
import net.sf.plist.io.PropertyListParser;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <i>07 04, 2012</i>
|
||||||
|
*
|
||||||
|
* @author lhunath
|
||||||
|
*/
|
||||||
|
public class MPTemplates extends MetaObject {
|
||||||
|
|
||||||
|
static final Logger logger = Logger.get( MPTemplates.class );
|
||||||
|
|
||||||
|
private final Map<MPElementType, List<MPTemplate>> templates;
|
||||||
|
|
||||||
|
public MPTemplates(final Map<MPElementType, List<MPTemplate>> templates) {
|
||||||
|
|
||||||
|
this.templates = templates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MPTemplates loadFromPList(final String templateResource) {
|
||||||
|
|
||||||
|
@SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
|
||||||
|
InputStream templateStream = Thread.currentThread().getContextClassLoader().getResourceAsStream( templateResource );
|
||||||
|
try {
|
||||||
|
NSObject plistObject = PropertyListParser.parse( templateStream );
|
||||||
|
Preconditions.checkState( NSDictionary.class.isAssignableFrom( plistObject.getClass() ) );
|
||||||
|
NSDictionary plist = (NSDictionary) plistObject;
|
||||||
|
|
||||||
|
NSDictionary characterClassesDict = (NSDictionary) plist.get( "MPCharacterClasses" );
|
||||||
|
NSDictionary templatesDict = (NSDictionary) plist.get( "MPElementGeneratedEntity" );
|
||||||
|
|
||||||
|
ImmutableMap.Builder<Character, MPTemplateCharacterClass> characterClassesBuilder = ImmutableMap.builder();
|
||||||
|
for (final Map.Entry<String, NSObject> characterClassEntry : characterClassesDict.entrySet()) {
|
||||||
|
String key = characterClassEntry.getKey();
|
||||||
|
NSObject value = characterClassEntry.getValue();
|
||||||
|
Preconditions.checkState( key.length() == 1 );
|
||||||
|
Preconditions.checkState( NSString.class.isAssignableFrom( value.getClass() ));
|
||||||
|
|
||||||
|
char character = key.charAt( 0 );
|
||||||
|
char[] characterClass = ((NSString)value).getValue().toCharArray();
|
||||||
|
characterClassesBuilder.put( character, new MPTemplateCharacterClass( character, characterClass ) );
|
||||||
|
}
|
||||||
|
ImmutableMap<Character, MPTemplateCharacterClass> characterClasses = characterClassesBuilder.build();
|
||||||
|
|
||||||
|
ImmutableMap.Builder<MPElementType, List<MPTemplate>> templatesBuilder = ImmutableMap.builder();
|
||||||
|
for (final Map.Entry<String, NSObject> template : templatesDict.entrySet()) {
|
||||||
|
String key = template.getKey();
|
||||||
|
NSObject value = template.getValue();
|
||||||
|
Preconditions.checkState( NSArray.class.isAssignableFrom( value.getClass() ) );
|
||||||
|
|
||||||
|
MPElementType type = MPElementType.forName( key );
|
||||||
|
List<NSObject> templateStrings = ((NSArray) value).getValue();
|
||||||
|
|
||||||
|
ImmutableList.Builder<MPTemplate> typeTemplatesBuilder = ImmutableList.<MPTemplate>builder();
|
||||||
|
for (final NSObject templateString : templateStrings)
|
||||||
|
typeTemplatesBuilder.add( new MPTemplate( ((NSString) templateString).getValue(), characterClasses ) );
|
||||||
|
|
||||||
|
templatesBuilder.put( type, typeTemplatesBuilder.build() );
|
||||||
|
}
|
||||||
|
ImmutableMap<MPElementType, List<MPTemplate>> templates = templatesBuilder.build();
|
||||||
|
|
||||||
|
return new MPTemplates( templates );
|
||||||
|
}
|
||||||
|
catch (PropertyListException e) {
|
||||||
|
logger.err( e, "Could not parse templates from: %s", templateResource );
|
||||||
|
throw Throwables.propagate( e );
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
logger.err( e, "Could not read templates from: %s", templateResource );
|
||||||
|
throw Throwables.propagate( e );
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Closeables.closeQuietly( templateStream );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public MPTemplate getTemplateForTypeAtRollingIndex(final MPElementType type, final int templateIndex) {
|
||||||
|
|
||||||
|
List<MPTemplate> typeTemplates = templates.get( type );
|
||||||
|
|
||||||
|
return typeTemplates.get( templateIndex % typeTemplates.size() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(final String... arguments) {
|
||||||
|
|
||||||
|
loadFromPList( "templates.plist" );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package com.lyndir.lhunath.masterpassword;
|
||||||
|
|
||||||
|
import com.google.common.base.Charsets;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.primitives.Bytes;
|
||||||
|
import com.lambdaworks.crypto.SCrypt;
|
||||||
|
import com.lyndir.lhunath.opal.crypto.CryptUtils;
|
||||||
|
import com.lyndir.lhunath.opal.system.*;
|
||||||
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the Master Password algorithm.
|
||||||
|
*
|
||||||
|
* <i>07 04, 2012</i>
|
||||||
|
*
|
||||||
|
* @author lhunath
|
||||||
|
*/
|
||||||
|
public abstract class MasterPassword {
|
||||||
|
|
||||||
|
static final Logger logger = Logger.get( MasterPassword.class );
|
||||||
|
private static final int MP_N = 32768;
|
||||||
|
private static final int MP_r = 8;
|
||||||
|
private static final int MP_p = 2;
|
||||||
|
private static final int MP_dkLen = 64;
|
||||||
|
private static final Charset MP_charset = Charsets.UTF_8;
|
||||||
|
private static final ByteOrder MP_byteOrder = ByteOrder.BIG_ENDIAN;
|
||||||
|
private static final MessageDigests MP_hash = MessageDigests.SHA256;
|
||||||
|
private static final MessageAuthenticationDigests MP_mac = MessageAuthenticationDigests.HmacSHA256;
|
||||||
|
private static final MPTemplates templates = MPTemplates.loadFromPList( "templates.plist" );
|
||||||
|
|
||||||
|
public static byte[] keyForPassword(final String password, final String username) {
|
||||||
|
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
byte[] nusernameLengthBytes = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE )
|
||||||
|
.order( MP_byteOrder )
|
||||||
|
.putInt( username.length() )
|
||||||
|
.array();
|
||||||
|
byte[] salt = Bytes.concat( "com.lyndir.masterpassword".getBytes( MP_charset ), //
|
||||||
|
nusernameLengthBytes, //
|
||||||
|
username.getBytes( MP_charset ) );
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] key = SCrypt.scrypt( password.getBytes( MP_charset ), salt, MP_N, MP_r, MP_p, MP_dkLen );
|
||||||
|
logger.trc( "User: %s, password: %s derives to key ID: %s (took %.2fs)", username, password,
|
||||||
|
CodeUtils.encodeHex( keyIDForKey( key ) ), (double) (System.currentTimeMillis() - start) / 1000 );
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
catch (GeneralSecurityException e) {
|
||||||
|
throw logger.bug( e );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] subkeyForKey(final byte[] key, final int subkeyLength) {
|
||||||
|
|
||||||
|
byte[] subkey = new byte[Math.min( subkeyLength, key.length )];
|
||||||
|
System.arraycopy( key, 0, subkey, 0, subkey.length );
|
||||||
|
|
||||||
|
return subkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] keyIDForPassword(final String password, final String username) {
|
||||||
|
|
||||||
|
return keyIDForKey( keyForPassword( password, username ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] keyIDForKey(final byte[] key) {
|
||||||
|
|
||||||
|
return MP_hash.of( key );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String generateContent(final MPElementType type, final String name, final byte[] key, int counter) {
|
||||||
|
|
||||||
|
Preconditions.checkArgument( type.getTypeClass() == MPElementTypeClass.Generated );
|
||||||
|
Preconditions.checkArgument( !name.isEmpty() );
|
||||||
|
Preconditions.checkArgument( key.length > 0 );
|
||||||
|
|
||||||
|
if (counter == 0)
|
||||||
|
counter = (int) (System.currentTimeMillis() / (300 * 1000)) * 300;
|
||||||
|
|
||||||
|
byte[] nameLengthBytes = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( MP_byteOrder ).putInt( name.length() ).array();
|
||||||
|
byte[] counterBytes = ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( MP_byteOrder ).putInt( counter ).array();
|
||||||
|
logger.trc( "seed from: hmac-sha256(%s, 'com.lyndir.masterpassword' | %s | %s | %s)", CryptUtils.encodeBase64( key ),
|
||||||
|
CodeUtils.encodeHex( nameLengthBytes ), name, CodeUtils.encodeHex( counterBytes ) );
|
||||||
|
byte[] seed = MP_mac.of( key, Bytes.concat( "com.lyndir.masterpassword".getBytes( MP_charset ), //
|
||||||
|
nameLengthBytes, //
|
||||||
|
name.getBytes( MP_charset ), //
|
||||||
|
counterBytes ) );
|
||||||
|
logger.trc( "seed is: %s", CryptUtils.encodeBase64( seed ) );
|
||||||
|
|
||||||
|
Preconditions.checkState( seed.length > 0 );
|
||||||
|
int templateIndex = seed[0] & 0xFF; // Mask the integer's sign.
|
||||||
|
MPTemplate template = templates.getTemplateForTypeAtRollingIndex( type, templateIndex );
|
||||||
|
logger.trc( "type: %s, template: %s", type, template );
|
||||||
|
|
||||||
|
StringBuilder password = new StringBuilder( template.length() );
|
||||||
|
for (int i = 0; i < template.length(); ++i) {
|
||||||
|
int characterIndex = seed[i + 1] & 0xFF; // Mask the integer's sign.
|
||||||
|
MPTemplateCharacterClass characterClass = template.getCharacterClassAtIndex( i );
|
||||||
|
char passwordCharacter = characterClass.getCharacterAtRollingIndex( characterIndex );
|
||||||
|
logger.trc( "class: %s, index: %d, byte: 0x%02X, chosen password character: %s", characterClass, characterIndex, seed[i + 1],
|
||||||
|
passwordCharacter );
|
||||||
|
|
||||||
|
password.append( passwordCharacter );
|
||||||
|
}
|
||||||
|
|
||||||
|
return password.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(final String... arguments) {
|
||||||
|
|
||||||
|
String masterPassword = "test-mp";
|
||||||
|
String username = "test-user";
|
||||||
|
String siteName = "test-site";
|
||||||
|
MPElementType siteType = MPElementType.GeneratedLong;
|
||||||
|
int siteCounter = 42;
|
||||||
|
|
||||||
|
String sitePassword = generateContent( siteType, siteName, keyForPassword( masterPassword, username ), siteCounter );
|
||||||
|
|
||||||
|
logger.inf( "master password: %s, username: %s\nsite name: %s, site type: %s, site counter: %d\n => site password: %s",
|
||||||
|
masterPassword, username, siteName, siteType, siteCounter, sitePassword );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.lyndir.lhunath.masterpassword.entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <i>07 04, 2012</i>
|
||||||
|
*
|
||||||
|
* @author lhunath
|
||||||
|
*/
|
||||||
|
public class MPElementEntity {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.lyndir.lhunath.masterpassword.entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <i>07 04, 2012</i>
|
||||||
|
*
|
||||||
|
* @author lhunath
|
||||||
|
*/
|
||||||
|
public class MPElementGeneratedEntity extends MPElementEntity {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.lyndir.lhunath.masterpassword.entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <i>07 04, 2012</i>
|
||||||
|
*
|
||||||
|
* @author lhunath
|
||||||
|
*/
|
||||||
|
public class MPElementStoredEntity extends MPElementEntity {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../../../../Resources/ciphers.plist
|
||||||
80
MasterPassword/Java/masterpassword-cli/pom.xml
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<!-- PROJECT METADATA -->
|
||||||
|
<parent>
|
||||||
|
<groupId>com.lyndir.lhunath.masterpassword</groupId>
|
||||||
|
<artifactId>masterpassword</artifactId>
|
||||||
|
<version>GIT-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<name>Master Password CLI</name>
|
||||||
|
<description>A CLI interface to the Master Password algorithm</description>
|
||||||
|
|
||||||
|
<groupId>com.lyndir.lhunath.masterpassword</groupId>
|
||||||
|
<artifactId>masterpassword-cli</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<!-- BUILD CONFIGURATION -->
|
||||||
|
<build>
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<directory>src/main/scripts</directory>
|
||||||
|
<filtering>true</filtering>
|
||||||
|
<targetPath>${project.build.directory}</targetPath>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>com.lyndir.lhunath.masterpassword.CLI</mainClass>
|
||||||
|
<addClasspath>true</addClasspath>
|
||||||
|
<classpathPrefix>lib/</classpathPrefix>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-dependency-plugin</artifactId>
|
||||||
|
<version>2.4</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>copy-dependencies</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>copy-dependencies</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<outputDirectory>${project.build.directory}/lib</outputDirectory>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<!-- DEPENDENCY MANAGEMENT -->
|
||||||
|
<dependencies>
|
||||||
|
|
||||||
|
<!-- PROJECT REFERENCES -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.lyndir.lhunath.masterpassword</groupId>
|
||||||
|
<artifactId>masterpassword-algorithm</artifactId>
|
||||||
|
<version>GIT-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.qos.logback</groupId>
|
||||||
|
<artifactId>logback-classic</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2008, Maarten Billemont
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.lyndir.lhunath.masterpassword;
|
||||||
|
|
||||||
|
import com.google.common.io.LineReader;
|
||||||
|
import com.lyndir.lhunath.opal.system.logging.Logger;
|
||||||
|
import com.lyndir.lhunath.opal.system.util.ConversionUtils;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p> <i>Jun 10, 2008</i> </p>
|
||||||
|
*
|
||||||
|
* @author mbillemo
|
||||||
|
*/
|
||||||
|
public class CLI {
|
||||||
|
|
||||||
|
static final Logger logger = Logger.get( CLI.class );
|
||||||
|
private static final String ENV_USERNAME = "MP_USERNAME";
|
||||||
|
private static final String ENV_PASSWORD = "MP_PASSWORD";
|
||||||
|
|
||||||
|
public static void main(final String[] args)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
String userName, masterPassword, siteName = null;
|
||||||
|
|
||||||
|
/* Environment. */
|
||||||
|
userName = System.getenv().get( ENV_USERNAME );
|
||||||
|
masterPassword = System.getenv().get( ENV_PASSWORD );
|
||||||
|
|
||||||
|
/* Arguments. */
|
||||||
|
int counter = 1;
|
||||||
|
MPElementType type = MPElementType.GeneratedLong;
|
||||||
|
boolean typeArg = false, counterArg = false, userNameArg = false;
|
||||||
|
for (final String arg : Arrays.asList( args ))
|
||||||
|
if ("-t".equals( arg ) || "--type".equals( arg ))
|
||||||
|
typeArg = true;
|
||||||
|
else if (typeArg) {
|
||||||
|
if ("list".equalsIgnoreCase( arg )) {
|
||||||
|
System.out.format( "%30s | %s\n", "type", "description" );
|
||||||
|
for (final MPElementType aType : MPElementType.values())
|
||||||
|
System.out.format( "%30s | %s\n", aType.getName(), aType.getDescription() );
|
||||||
|
System.exit( 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
type = MPElementType.forName( arg );
|
||||||
|
typeArg = false;
|
||||||
|
} else if ("-c".equals( arg ) || "--counter".equals( arg ))
|
||||||
|
counterArg = true;
|
||||||
|
else if (counterArg) {
|
||||||
|
counter = ConversionUtils.toIntegerNN( arg );
|
||||||
|
counterArg = false;
|
||||||
|
} else if ("-u".equals( arg ) || "--username".equals( arg ))
|
||||||
|
userNameArg = true;
|
||||||
|
else if (userNameArg) {
|
||||||
|
userName = arg;
|
||||||
|
userNameArg = false;
|
||||||
|
} else if ("-h".equals( arg ) || "--help".equals( arg )) {
|
||||||
|
System.out.println();
|
||||||
|
System.out.println( "\tMaster Password CLI" );
|
||||||
|
System.out.println( "\t\tLyndir" );
|
||||||
|
|
||||||
|
System.out.println( "[options] [site name]" );
|
||||||
|
System.out.println();
|
||||||
|
System.out.println( "Available options:" );
|
||||||
|
|
||||||
|
System.out.println( "\t-t | --type [site password type]" );
|
||||||
|
System.out.format( "\t\tDefault: %s. The password type to use for this site.\n", type.getName() );
|
||||||
|
System.out.println( "\t\tUse 'list' to see the available types." );
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println( "\t-c | --counter [site counter]" );
|
||||||
|
System.out.format( "\t\tDefault: %d. The counter to use for this site.\n", counter );
|
||||||
|
System.out.println( "\t\tIncrement the counter if you need a new password." );
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println( "\t-u | --username [user's name]" );
|
||||||
|
System.out.println( "\t\tDefault: asked. The name of the user." );
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println( "Available environment variables:" );
|
||||||
|
|
||||||
|
System.out.format( "\t%s\n", ENV_USERNAME );
|
||||||
|
System.out.println( "\t\tThe name of the user." );
|
||||||
|
|
||||||
|
System.out.format( "\t%s\n", ENV_PASSWORD );
|
||||||
|
System.out.println( "\t\tThe master password of the user." );
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
return;
|
||||||
|
} else
|
||||||
|
siteName = arg;
|
||||||
|
|
||||||
|
InputStreamReader inReader = new InputStreamReader( System.in );
|
||||||
|
try {
|
||||||
|
LineReader lineReader = new LineReader( inReader );
|
||||||
|
if (siteName == null) {
|
||||||
|
System.err.format( "Site name: " );
|
||||||
|
siteName = lineReader.readLine();
|
||||||
|
}
|
||||||
|
if (userName == null) {
|
||||||
|
System.err.format( "User's name: " );
|
||||||
|
userName = lineReader.readLine();
|
||||||
|
}
|
||||||
|
if (masterPassword == null) {
|
||||||
|
System.err.format( "%s's master password: ", userName );
|
||||||
|
masterPassword = lineReader.readLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] masterKey = MasterPassword.keyForPassword( masterPassword, userName );
|
||||||
|
String sitePassword = MasterPassword.generateContent( type, siteName, masterKey, counter );
|
||||||
|
System.out.println( sitePassword );
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
inReader.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<configuration scan="true">
|
||||||
|
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||||
|
<Pattern>%-8relative %22c{0} [%-5level] %msg%n</Pattern>
|
||||||
|
</layout>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<logger name="com.lyndir" level="TRACE" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<logger name="org.apache.wicket" level="DEBUG" />
|
||||||
|
-->
|
||||||
|
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="STDOUT" />
|
||||||
|
</root>
|
||||||
|
|
||||||
|
</configuration>
|
||||||
4
MasterPassword/Java/masterpassword-cli/src/main/scripts/mpw
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
cd "${BASH_SOURCE[0]%/*}"
|
||||||
|
java -jar masterpassword-cli-GIT-SNAPSHOT.jar "$@"
|
||||||
26
MasterPassword/Java/pom.xml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<!-- PROJECT METADATA -->
|
||||||
|
<parent>
|
||||||
|
<groupId>com.lyndir.lhunath</groupId>
|
||||||
|
<artifactId>lyndir</artifactId>
|
||||||
|
<version>GIT-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<name>Master Password</name>
|
||||||
|
<description>A Java implementation of the Master Password algorithm.</description>
|
||||||
|
|
||||||
|
<groupId>com.lyndir.lhunath.masterpassword</groupId>
|
||||||
|
<artifactId>masterpassword</artifactId>
|
||||||
|
<version>GIT-SNAPSHOT</version>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>masterpassword-algorithm</module>
|
||||||
|
<module>masterpassword-cli</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
|
</project>
|
||||||
44
MasterPassword/MPAlgorithm.h
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
|
||||||
|
*
|
||||||
|
* See the enclosed file LICENSE for license information (LGPLv3). If you did
|
||||||
|
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*
|
||||||
|
* @author Maarten Billemont <lhunath@lyndir.com>
|
||||||
|
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
//
|
||||||
|
// MPAlgorithm
|
||||||
|
//
|
||||||
|
// Created by Maarten Billemont on 16/07/12.
|
||||||
|
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "MPKey.h"
|
||||||
|
#import "MPElementGeneratedEntity.h"
|
||||||
|
|
||||||
|
#define MPAlgorithmDefaultVersion 1
|
||||||
|
#define MPAlgorithmDefault MPAlgorithmForVersion(MPAlgorithmDefaultVersion)
|
||||||
|
|
||||||
|
@protocol MPAlgorithm<NSObject>
|
||||||
|
|
||||||
|
@required
|
||||||
|
- (NSUInteger)version;
|
||||||
|
- (BOOL)migrateElement:(MPElementEntity *)element explicit:(BOOL)explicit;
|
||||||
|
|
||||||
|
- (MPKey *)keyForPassword:(NSString *)password ofUserNamed:(NSString *)userName;
|
||||||
|
- (MPKey *)keyFromKeyData:(NSData *)keyData;
|
||||||
|
- (NSData *)keyIDForKeyData:(NSData *)keyData;
|
||||||
|
|
||||||
|
- (NSString *)nameOfType:(MPElementType)type;
|
||||||
|
- (NSString *)shortNameOfType:(MPElementType)type;
|
||||||
|
- (NSString *)classNameOfType:(MPElementType)type;
|
||||||
|
- (Class)classOfType:(MPElementType)type;
|
||||||
|
|
||||||
|
- (NSString *)generateContentForElement:(MPElementGeneratedEntity *)element usingKey:(MPKey *)key;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
id<MPAlgorithm> MPAlgorithmForVersion(NSUInteger version);
|
||||||
|
id<MPAlgorithm> MPAlgorithmDefaultForBundleVersion(NSString *bundleVersion);
|
||||||
42
MasterPassword/MPAlgorithm.m
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
|
||||||
|
*
|
||||||
|
* See the enclosed file LICENSE for license information (LGPLv3). If you did
|
||||||
|
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*
|
||||||
|
* @author Maarten Billemont <lhunath@lyndir.com>
|
||||||
|
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
//
|
||||||
|
// MPAlgorithm
|
||||||
|
//
|
||||||
|
// Created by Maarten Billemont on 16/07/12.
|
||||||
|
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "MPAlgorithm.h"
|
||||||
|
#import "MPEntities.h"
|
||||||
|
|
||||||
|
id<MPAlgorithm> MPAlgorithmForVersion(NSUInteger version) {
|
||||||
|
|
||||||
|
static NSMutableDictionary *versionToAlgorithm = nil;
|
||||||
|
if (!versionToAlgorithm)
|
||||||
|
versionToAlgorithm = [NSMutableDictionary dictionary];
|
||||||
|
|
||||||
|
id<MPAlgorithm> algorithm = [versionToAlgorithm objectForKey:PearlUnsignedInteger(version)];
|
||||||
|
if (!algorithm)
|
||||||
|
if ((algorithm = [NSClassFromString(PearlString(@"MPAlgorithmV%u", version)) new]))
|
||||||
|
[versionToAlgorithm setObject:algorithm forKey:PearlUnsignedInteger(version)];
|
||||||
|
|
||||||
|
return algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
id<MPAlgorithm> MPAlgorithmDefaultForBundleVersion(NSString *bundleVersion) {
|
||||||
|
|
||||||
|
if (PearlCFBundleVersionCompare(bundleVersion, @"1.3") == NSOrderedAscending)
|
||||||
|
// Pre-1.3
|
||||||
|
return MPAlgorithmForVersion(0);
|
||||||
|
|
||||||
|
return MPAlgorithmDefault;
|
||||||
|
}
|
||||||
21
MasterPassword/MPAlgorithmV0.h
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
|
||||||
|
*
|
||||||
|
* See the enclosed file LICENSE for license information (LGPLv3). If you did
|
||||||
|
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*
|
||||||
|
* @author Maarten Billemont <lhunath@lyndir.com>
|
||||||
|
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
//
|
||||||
|
// MPAlgorithmV0
|
||||||
|
//
|
||||||
|
// Created by Maarten Billemont on 16/07/12.
|
||||||
|
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "MPAlgorithm.h"
|
||||||
|
|
||||||
|
@interface MPAlgorithmV0 : NSObject <MPAlgorithm>
|
||||||
|
@end
|
||||||
250
MasterPassword/MPAlgorithmV0.m
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
/**
|
||||||
|
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
|
||||||
|
*
|
||||||
|
* See the enclosed file LICENSE for license information (LGPLv3). If you did
|
||||||
|
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*
|
||||||
|
* @author Maarten Billemont <lhunath@lyndir.com>
|
||||||
|
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
//
|
||||||
|
// MPAlgorithmV0
|
||||||
|
//
|
||||||
|
// Created by Maarten Billemont on 16/07/12.
|
||||||
|
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "MPAlgorithmV0.h"
|
||||||
|
#import "MPEntities.h"
|
||||||
|
|
||||||
|
#define MP_N 32768
|
||||||
|
#define MP_r 8
|
||||||
|
#define MP_p 2
|
||||||
|
#define MP_dkLen 64
|
||||||
|
#define MP_hash PearlHashSHA256
|
||||||
|
|
||||||
|
@implementation MPAlgorithmV0
|
||||||
|
|
||||||
|
- (NSUInteger)version {
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)migrateElement:(MPElementEntity *)element explicit:(BOOL)explicit {
|
||||||
|
|
||||||
|
if (element.version != [self version] - 1)
|
||||||
|
// Only migrate from previous version.
|
||||||
|
return NO;
|
||||||
|
|
||||||
|
if (!explicit) {
|
||||||
|
// This migration requires explicit permission.
|
||||||
|
element.requiresExplicitMigration = YES;
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply migration.
|
||||||
|
element.requiresExplicitMigration = NO;
|
||||||
|
element.version = [self version];
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MPKey *)keyForPassword:(NSString *)password ofUserNamed:(NSString *)userName {
|
||||||
|
|
||||||
|
uint32_t nuserNameLength = htonl(userName.length);
|
||||||
|
NSDate *start = [NSDate date];
|
||||||
|
NSData *keyData = [PearlSCrypt deriveKeyWithLength:MP_dkLen fromPassword:[password dataUsingEncoding:NSUTF8StringEncoding]
|
||||||
|
usingSalt:[NSData dataByConcatenatingDatas:
|
||||||
|
[@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding],
|
||||||
|
[NSData dataWithBytes:&nuserNameLength
|
||||||
|
length:sizeof(nuserNameLength)],
|
||||||
|
[userName dataUsingEncoding:NSUTF8StringEncoding],
|
||||||
|
nil] N:MP_N r:MP_r p:MP_p];
|
||||||
|
|
||||||
|
MPKey *key = [self keyFromKeyData:keyData];
|
||||||
|
trc(@"User: %@, password: %@ derives to key ID: %@ (took %0.2fs)", userName, password, [key.keyID encodeHex], -[start timeIntervalSinceNow]);
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MPKey *)keyFromKeyData:(NSData *)keyData {
|
||||||
|
|
||||||
|
return [[MPKey alloc] initWithKeyData:keyData algorithm:self];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSData *)keyIDForKeyData:(NSData *)keyData {
|
||||||
|
|
||||||
|
return [keyData hashWith:MP_hash];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)nameOfType:(MPElementType)type {
|
||||||
|
|
||||||
|
if (!type)
|
||||||
|
return nil;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case MPElementTypeGeneratedMaximum:
|
||||||
|
return @"Maximum Security Password";
|
||||||
|
|
||||||
|
case MPElementTypeGeneratedLong:
|
||||||
|
return @"Long Password";
|
||||||
|
|
||||||
|
case MPElementTypeGeneratedMedium:
|
||||||
|
return @"Medium Password";
|
||||||
|
|
||||||
|
case MPElementTypeGeneratedShort:
|
||||||
|
return @"Short Password";
|
||||||
|
|
||||||
|
case MPElementTypeGeneratedBasic:
|
||||||
|
return @"Basic Password";
|
||||||
|
|
||||||
|
case MPElementTypeGeneratedPIN:
|
||||||
|
return @"PIN";
|
||||||
|
|
||||||
|
case MPElementTypeStoredPersonal:
|
||||||
|
return @"Personal Password";
|
||||||
|
|
||||||
|
case MPElementTypeStoredDevicePrivate:
|
||||||
|
return @"Device Private Password";
|
||||||
|
}
|
||||||
|
|
||||||
|
Throw(@"Type not supported: %d", type);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)shortNameOfType:(MPElementType)type {
|
||||||
|
|
||||||
|
if (!type)
|
||||||
|
return nil;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case MPElementTypeGeneratedMaximum:
|
||||||
|
return @"Maximum";
|
||||||
|
|
||||||
|
case MPElementTypeGeneratedLong:
|
||||||
|
return @"Long";
|
||||||
|
|
||||||
|
case MPElementTypeGeneratedMedium:
|
||||||
|
return @"Medium";
|
||||||
|
|
||||||
|
case MPElementTypeGeneratedShort:
|
||||||
|
return @"Short";
|
||||||
|
|
||||||
|
case MPElementTypeGeneratedBasic:
|
||||||
|
return @"Basic";
|
||||||
|
|
||||||
|
case MPElementTypeGeneratedPIN:
|
||||||
|
return @"PIN";
|
||||||
|
|
||||||
|
case MPElementTypeStoredPersonal:
|
||||||
|
return @"Personal";
|
||||||
|
|
||||||
|
case MPElementTypeStoredDevicePrivate:
|
||||||
|
return @"Device";
|
||||||
|
}
|
||||||
|
|
||||||
|
Throw(@"Type not supported: %d", type);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)classNameOfType:(MPElementType)type {
|
||||||
|
|
||||||
|
return NSStringFromClass([self classOfType:type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (Class)classOfType:(MPElementType)type {
|
||||||
|
|
||||||
|
if (!type)
|
||||||
|
return nil;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case MPElementTypeGeneratedMaximum:
|
||||||
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
|
case MPElementTypeGeneratedLong:
|
||||||
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
|
case MPElementTypeGeneratedMedium:
|
||||||
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
|
case MPElementTypeGeneratedShort:
|
||||||
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
|
case MPElementTypeGeneratedBasic:
|
||||||
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
|
case MPElementTypeGeneratedPIN:
|
||||||
|
return [MPElementGeneratedEntity class];
|
||||||
|
|
||||||
|
case MPElementTypeStoredPersonal:
|
||||||
|
return [MPElementStoredEntity class];
|
||||||
|
|
||||||
|
case MPElementTypeStoredDevicePrivate:
|
||||||
|
return [MPElementStoredEntity class];
|
||||||
|
}
|
||||||
|
|
||||||
|
Throw(@"Type not supported: %d", type);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)generateContentForElement:(MPElementGeneratedEntity *)element usingKey:(MPKey *)key {
|
||||||
|
|
||||||
|
static NSDictionary *MPTypes_ciphers = nil;
|
||||||
|
|
||||||
|
if (!element)
|
||||||
|
return nil;
|
||||||
|
|
||||||
|
if (!(element.type & MPElementTypeClassGenerated)) {
|
||||||
|
err(@"Incorrect type (is not MPElementTypeClassGenerated): %@, for: %@", [self nameOfType:element.type], element.name);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
if (!element.name.length) {
|
||||||
|
err(@"Missing name.");
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
if (!key.keyData.length) {
|
||||||
|
err(@"Missing key.");
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MPTypes_ciphers == nil)
|
||||||
|
MPTypes_ciphers = [NSDictionary dictionaryWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"ciphers"
|
||||||
|
withExtension:@"plist"]];
|
||||||
|
|
||||||
|
// Determine the seed whose bytes will be used for calculating a password
|
||||||
|
uint32_t ncounter = htonl(element.counter), nnameLength = htonl(element.name.length);
|
||||||
|
NSData *counterBytes = [NSData dataWithBytes:&ncounter length:sizeof(ncounter)];
|
||||||
|
NSData *nameLengthBytes = [NSData dataWithBytes:&nnameLength length:sizeof(nnameLength)];
|
||||||
|
trc(@"seed from: hmac-sha256(%@, 'com.lyndir.masterpassword' | %@ | %@ | %@)", [key.keyData encodeBase64], [nameLengthBytes encodeHex], element.name, [counterBytes encodeHex]);
|
||||||
|
NSData *seed = [[NSData dataByConcatenatingDatas:
|
||||||
|
[@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding],
|
||||||
|
nameLengthBytes,
|
||||||
|
[element.name dataUsingEncoding:NSUTF8StringEncoding],
|
||||||
|
counterBytes,
|
||||||
|
nil]
|
||||||
|
hmacWith:PearlHashSHA256 key:key.keyData];
|
||||||
|
trc(@"seed is: %@", [seed encodeBase64]);
|
||||||
|
const char *seedBytes = seed.bytes;
|
||||||
|
|
||||||
|
// Determine the cipher from the first seed byte.
|
||||||
|
assert([seed length]);
|
||||||
|
NSArray *typeCiphers = [[MPTypes_ciphers valueForKey:[self classNameOfType:element.type]]
|
||||||
|
valueForKey:[self nameOfType:element.type]];
|
||||||
|
NSString *cipher = [typeCiphers objectAtIndex:htons(seedBytes[0]) % [typeCiphers count]];
|
||||||
|
trc(@"type %@, ciphers: %@, selected: %@", [self nameOfType:element.type], typeCiphers, cipher);
|
||||||
|
|
||||||
|
// Encode the content, character by character, using subsequent seed bytes and the cipher.
|
||||||
|
assert([seed length] >= [cipher length] + 1);
|
||||||
|
NSMutableString *content = [NSMutableString stringWithCapacity:[cipher length]];
|
||||||
|
for (NSUInteger c = 0; c < [cipher length]; ++c) {
|
||||||
|
uint16_t keyByte = htons(seedBytes[c + 1]);
|
||||||
|
NSString *cipherClass = [cipher substringWithRange:NSMakeRange(c, 1)];
|
||||||
|
NSString *cipherClassCharacters = [[MPTypes_ciphers valueForKey:@"MPCharacterClasses"] valueForKey:cipherClass];
|
||||||
|
NSString *character = [cipherClassCharacters substringWithRange:NSMakeRange(keyByte % [cipherClassCharacters length],
|
||||||
|
1)];
|
||||||
|
|
||||||
|
trc(@"class %@ has characters: %@, index: %u, selected: %@", cipherClass, cipherClassCharacters, keyByte, character);
|
||||||
|
[content appendString:character];
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
22
MasterPassword/MPAlgorithmV1.h
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
|
||||||
|
*
|
||||||
|
* See the enclosed file LICENSE for license information (LGPLv3). If you did
|
||||||
|
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*
|
||||||
|
* @author Maarten Billemont <lhunath@lyndir.com>
|
||||||
|
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
//
|
||||||
|
// MPAlgorithmV1
|
||||||
|
//
|
||||||
|
// Created by Maarten Billemont on 17/07/12.
|
||||||
|
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "MPAlgorithmV0.h"
|
||||||
|
|
||||||
|
|
||||||
|
@interface MPAlgorithmV1 : MPAlgorithmV0
|
||||||
|
@end
|
||||||
113
MasterPassword/MPAlgorithmV1.m
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
|
||||||
|
*
|
||||||
|
* See the enclosed file LICENSE for license information (LGPLv3). If you did
|
||||||
|
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*
|
||||||
|
* @author Maarten Billemont <lhunath@lyndir.com>
|
||||||
|
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
//
|
||||||
|
// MPAlgorithmV1
|
||||||
|
//
|
||||||
|
// Created by Maarten Billemont on 17/07/12.
|
||||||
|
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "MPAlgorithmV1.h"
|
||||||
|
#import "MPEntities.h"
|
||||||
|
|
||||||
|
|
||||||
|
@implementation MPAlgorithmV1
|
||||||
|
|
||||||
|
- (NSUInteger)version {
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)migrateElement:(MPElementEntity *)element explicit:(BOOL)explicit {
|
||||||
|
|
||||||
|
if (element.version != [self version] - 1)
|
||||||
|
// Only migrate from previous version.
|
||||||
|
return NO;
|
||||||
|
|
||||||
|
if (!explicit) {
|
||||||
|
if (element.type & MPElementTypeClassGenerated) {
|
||||||
|
// This migration requires explicit permission for types of the generated class.
|
||||||
|
element.requiresExplicitMigration = YES;
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply migration.
|
||||||
|
element.requiresExplicitMigration = NO;
|
||||||
|
element.version = [self version];
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)generateContentForElement:(MPElementGeneratedEntity *)element usingKey:(MPKey *)key {
|
||||||
|
|
||||||
|
static NSDictionary *MPTypes_ciphers = nil;
|
||||||
|
|
||||||
|
if (!element)
|
||||||
|
return nil;
|
||||||
|
|
||||||
|
if (!(element.type & MPElementTypeClassGenerated)) {
|
||||||
|
err(@"Incorrect type (is not MPElementTypeClassGenerated): %@, for: %@", [self nameOfType:element.type], element.name);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
if (!element.name.length) {
|
||||||
|
err(@"Missing name.");
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
if (!key.keyData.length) {
|
||||||
|
err(@"Missing key.");
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MPTypes_ciphers == nil)
|
||||||
|
MPTypes_ciphers = [NSDictionary dictionaryWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"ciphers"
|
||||||
|
withExtension:@"plist"]];
|
||||||
|
|
||||||
|
// Determine the seed whose bytes will be used for calculating a password
|
||||||
|
uint32_t ncounter = htonl(element.counter), nnameLength = htonl(element.name.length);
|
||||||
|
NSData *counterBytes = [NSData dataWithBytes:&ncounter length:sizeof(ncounter)];
|
||||||
|
NSData *nameLengthBytes = [NSData dataWithBytes:&nnameLength length:sizeof(nnameLength)];
|
||||||
|
trc(@"seed from: hmac-sha256(%@, 'com.lyndir.masterpassword' | %@ | %@ | %@)", [key.keyData encodeBase64], [nameLengthBytes encodeHex], element.name, [counterBytes encodeHex]);
|
||||||
|
NSData *seed = [[NSData dataByConcatenatingDatas:
|
||||||
|
[@"com.lyndir.masterpassword" dataUsingEncoding:NSUTF8StringEncoding],
|
||||||
|
nameLengthBytes,
|
||||||
|
[element.name dataUsingEncoding:NSUTF8StringEncoding],
|
||||||
|
counterBytes,
|
||||||
|
nil]
|
||||||
|
hmacWith:PearlHashSHA256 key:key.keyData];
|
||||||
|
trc(@"seed is: %@", [seed encodeBase64]);
|
||||||
|
const unsigned char *seedBytes = seed.bytes;
|
||||||
|
|
||||||
|
// Determine the cipher from the first seed byte.
|
||||||
|
assert([seed length]);
|
||||||
|
NSArray *typeCiphers = [[MPTypes_ciphers valueForKey:[self classNameOfType:element.type]]
|
||||||
|
valueForKey:[self nameOfType:element.type]];
|
||||||
|
NSString *cipher = [typeCiphers objectAtIndex:seedBytes[0] % [typeCiphers count]];
|
||||||
|
trc(@"type %@, ciphers: %@, selected: %@", [self nameOfType:element.type], typeCiphers, cipher);
|
||||||
|
|
||||||
|
// Encode the content, character by character, using subsequent seed bytes and the cipher.
|
||||||
|
assert([seed length] >= [cipher length] + 1);
|
||||||
|
NSMutableString *content = [NSMutableString stringWithCapacity:[cipher length]];
|
||||||
|
for (NSUInteger c = 0; c < [cipher length]; ++c) {
|
||||||
|
uint16_t keyByte = seedBytes[c + 1];
|
||||||
|
NSString *cipherClass = [cipher substringWithRange:NSMakeRange(c, 1)];
|
||||||
|
NSString *cipherClassCharacters = [[MPTypes_ciphers valueForKey:@"MPCharacterClasses"] valueForKey:cipherClass];
|
||||||
|
NSString *character = [cipherClassCharacters substringWithRange:NSMakeRange(keyByte % [cipherClassCharacters length],
|
||||||
|
1)];
|
||||||
|
|
||||||
|
trc(@"class %@ has characters: %@, index: %u, selected: %@", cipherClass, cipherClassCharacters, keyByte, character);
|
||||||
|
[content appendString:character];
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -10,13 +10,10 @@
|
|||||||
|
|
||||||
@interface MPAppDelegate_Shared (Key)
|
@interface MPAppDelegate_Shared (Key)
|
||||||
|
|
||||||
- (void)loadStoredKey;
|
- (BOOL)signInAsUser:(MPUserEntity *)user usingMasterPassword:(NSString *)password;
|
||||||
- (IBAction)signOut:(id)sender;
|
- (void)signOutAnimated:(BOOL)animated;
|
||||||
|
|
||||||
- (BOOL)tryMasterPassword:(NSString *)tryPassword;
|
- (void)storeSavedKeyFor:(MPUserEntity *)user;
|
||||||
- (void)updateKey:(NSData *)key;
|
- (void)forgetSavedKeyFor:(MPUserEntity *)user;
|
||||||
- (void)forgetKey;
|
|
||||||
|
|
||||||
- (NSData *)keyWithLength:(NSUInteger)keyLength;
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -6,153 +6,171 @@
|
|||||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "MPConfig.h"
|
#import <Crashlytics/Crashlytics.h>
|
||||||
#import "MPAppDelegate_Key.h"
|
#import "MPAppDelegate_Key.h"
|
||||||
#import "MPElementEntity.h"
|
#import "MPAppDelegate_Store.h"
|
||||||
|
#import "LocalyticsSession.h"
|
||||||
|
|
||||||
@implementation MPAppDelegate_Shared (Key)
|
@implementation MPAppDelegate_Shared (Key)
|
||||||
|
|
||||||
static NSDictionary *keyQuery() {
|
static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||||
|
|
||||||
static NSDictionary *MPKeyQuery = nil;
|
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
||||||
if (!MPKeyQuery)
|
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
MPKeyQuery = [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
@"Saved Master Password", (__bridge id)kSecAttrService,
|
||||||
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
user.name, (__bridge id)kSecAttrAccount,
|
||||||
@"Saved Master Password", (__bridge id)kSecAttrService,
|
nil]
|
||||||
@"default", (__bridge id)kSecAttrAccount,
|
matches:nil];
|
||||||
nil]
|
|
||||||
matches:nil];
|
|
||||||
|
|
||||||
return MPKeyQuery;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static NSDictionary *keyIDQuery() {
|
- (MPKey *)loadSavedKeyFor:(MPUserEntity *)user {
|
||||||
|
|
||||||
static NSDictionary *MPKeyIDQuery = nil;
|
NSData *keyData = [PearlKeyChain dataOfItemForQuery:keyQuery(user)];
|
||||||
if (!MPKeyIDQuery)
|
if (keyData)
|
||||||
MPKeyIDQuery = [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
inf(@"Found key in keychain for: %@", user.userID);
|
||||||
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
|
||||||
@"Master Password Check", (__bridge id)kSecAttrService,
|
|
||||||
@"default", (__bridge id)kSecAttrAccount,
|
|
||||||
nil]
|
|
||||||
matches:nil];
|
|
||||||
|
|
||||||
return MPKeyIDQuery;
|
else {
|
||||||
|
user.saveKey = NO;
|
||||||
|
inf(@"No key found in keychain for: %@", user.userID);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [MPAlgorithmDefault keyFromKeyData:keyData];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)forgetKey {
|
- (void)storeSavedKeyFor:(MPUserEntity *)user {
|
||||||
|
|
||||||
inf(@"Deleting key and ID from keychain.");
|
if (user.saveKey) {
|
||||||
if ([PearlKeyChain deleteItemForQuery:keyQuery()] != errSecItemNotFound)
|
NSData *existingKeyData = [PearlKeyChain dataOfItemForQuery:keyQuery(user)];
|
||||||
inf(@"Removed key from keychain.");
|
|
||||||
if ([PearlKeyChain deleteItemForQuery:keyIDQuery()] != errSecItemNotFound)
|
|
||||||
inf(@"Removed key ID from keychain.");
|
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyForgotten object:self];
|
if (![existingKeyData isEqualToData:self.key.keyData]) {
|
||||||
#ifdef TESTFLIGHT_SDK_VERSION
|
inf(@"Saving key in keychain for: %@", user.userID);
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPForgotten];
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
- (IBAction)signOut:(id)sender {
|
[PearlKeyChain addOrUpdateItemForQuery:keyQuery(user)
|
||||||
|
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
[MPConfig get].saveKey = [NSNumber numberWithBool:NO];
|
self.key.keyData, (__bridge id)kSecValueData,
|
||||||
[self updateKey:nil];
|
#if TARGET_OS_IPHONE
|
||||||
}
|
(__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly, (__bridge id)kSecAttrAccessible,
|
||||||
|
#endif
|
||||||
- (void)loadStoredKey {
|
nil]];
|
||||||
|
}
|
||||||
if ([[MPConfig get].saveKey boolValue]) {
|
|
||||||
// Key is stored in keychain. Load it.
|
|
||||||
[self updateKey:[PearlKeyChain dataOfItemForQuery:keyQuery()]];
|
|
||||||
inf(@"Looking for key in keychain: %@.", self.key? @"found": @"missing");
|
|
||||||
} else {
|
|
||||||
// Key should not be stored in keychain. Delete it.
|
|
||||||
if ([PearlKeyChain deleteItemForQuery:keyQuery()] != errSecItemNotFound)
|
|
||||||
inf(@"Removed key from keychain.");
|
|
||||||
#ifdef TESTFLIGHT_SDK_VERSION
|
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPUnstored];
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)tryMasterPassword:(NSString *)tryPassword {
|
- (void)forgetSavedKeyFor:(MPUserEntity *)user {
|
||||||
|
|
||||||
if (![tryPassword length])
|
OSStatus result = [PearlKeyChain deleteItemForQuery:keyQuery(user)];
|
||||||
return NO;
|
if (result == noErr || result == errSecItemNotFound) {
|
||||||
|
user.saveKey = NO;
|
||||||
|
|
||||||
NSData *tryKey = keyForPassword(tryPassword);
|
if (result == noErr) {
|
||||||
NSData *tryKeyID = keyIDForKey(tryKey);
|
inf(@"Removed key from keychain for: %@", user.userID);
|
||||||
NSData *keyID = [PearlKeyChain dataOfItemForQuery:keyIDQuery()];
|
|
||||||
inf(@"Key ID known? %@.", keyID? @"YES": @"NO");
|
|
||||||
if (keyID)
|
|
||||||
// A key ID is known -> a password is set.
|
|
||||||
// Make sure the user's entered password matches it.
|
|
||||||
if (![keyID isEqual:tryKeyID]) {
|
|
||||||
wrn(@"Key ID mismatch. Expected: %@, answer: %@.", [keyID encodeHex], [tryKeyID encodeHex]);
|
|
||||||
|
|
||||||
|
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyForgotten object:self];
|
||||||
#ifdef TESTFLIGHT_SDK_VERSION
|
#ifdef TESTFLIGHT_SDK_VERSION
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPMismatch];
|
[TestFlight passCheckpoint:MPCheckpointForgetSavedKey];
|
||||||
#endif
|
#endif
|
||||||
return NO;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)signOutAnimated:(BOOL)animated {
|
||||||
|
|
||||||
|
if (self.key)
|
||||||
|
self.key = nil;
|
||||||
|
|
||||||
|
if (self.activeUser) {
|
||||||
|
self.activeUser = nil;
|
||||||
|
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationSignedOut object:self userInfo:
|
||||||
|
[NSDictionary dictionaryWithObject:PearlBool(animated) forKey:@"animated"]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)signInAsUser:(MPUserEntity *)user usingMasterPassword:(NSString *)password {
|
||||||
|
|
||||||
|
MPKey *tryKey = nil;
|
||||||
|
|
||||||
|
// Method 1: When the user has no keyID set, set a new key from the given master password.
|
||||||
|
if (!user.keyID) {
|
||||||
|
if ([password length])
|
||||||
|
if ((tryKey = [MPAlgorithmDefault keyForPassword:password ofUserNamed:user.name])) {
|
||||||
|
user.keyID = tryKey.keyID;
|
||||||
|
[[MPAppDelegate_Shared get] saveContext];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Depending on the user's saveKey, load or remove the key from the keychain.
|
||||||
|
if (!user.saveKey)
|
||||||
|
// Key should not be stored in keychain. Delete it.
|
||||||
|
[self forgetSavedKeyFor:user];
|
||||||
|
|
||||||
|
else
|
||||||
|
if (!tryKey) {
|
||||||
|
// Key should be saved in keychain. Load it.
|
||||||
|
if ((tryKey = [self loadSavedKeyFor:user]))
|
||||||
|
if (![user.keyID isEqual:tryKey.keyID]) {
|
||||||
|
// Loaded password doesn't match user's keyID. Forget saved password: it is incorrect.
|
||||||
|
inf(@"Saved password doesn't match keyID for: %@", user.userID);
|
||||||
|
|
||||||
|
tryKey = nil;
|
||||||
|
[self forgetSavedKeyFor:user];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef TESTFLIGHT_SDK_VERSION
|
// Method 3: Check the given master password string.
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPEntered];
|
if (!tryKey) {
|
||||||
#endif
|
if ([password length])
|
||||||
|
if ((tryKey = [MPAlgorithmDefault keyForPassword:password ofUserNamed:user.name]))
|
||||||
|
if (![user.keyID isEqual:tryKey.keyID]) {
|
||||||
|
inf(@"Key derived from password doesn't match keyID for: %@", user.userID);
|
||||||
|
|
||||||
|
tryKey = nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No more methods left, fail if key still not known.
|
||||||
|
if (!tryKey) {
|
||||||
|
if (password) {
|
||||||
|
inf(@"Login failed for: %@", user.userID);
|
||||||
|
|
||||||
|
#ifdef TESTFLIGHT_SDK_VERSION
|
||||||
|
[TestFlight passCheckpoint:MPCheckpointSignInFailed];
|
||||||
|
#endif
|
||||||
|
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointSignInFailed attributes:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
inf(@"Logged in: %@", user.userID);
|
||||||
|
|
||||||
|
if (![self.key isEqualToKey:tryKey]) {
|
||||||
|
self.key = tryKey;
|
||||||
|
[self storeSavedKeyFor:user];
|
||||||
|
}
|
||||||
|
|
||||||
|
@try {
|
||||||
|
if ([[MPiOSConfig get].sendInfo boolValue]) {
|
||||||
|
[TestFlight addCustomEnvironmentInformation:user.userID forKey:@"username"];
|
||||||
|
[[Crashlytics sharedInstance] setObjectValue:user.userID forKey:@"username"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@catch (id exception) {
|
||||||
|
err(@"While setting username: %@", exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
user.lastUsed = [NSDate date];
|
||||||
|
self.activeUser = user;
|
||||||
|
self.activeUser.requiresExplicitMigration = NO;
|
||||||
|
[[MPAppDelegate_Shared get] saveContext];
|
||||||
|
|
||||||
|
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationSignedIn object:self];
|
||||||
|
#ifdef TESTFLIGHT_SDK_VERSION
|
||||||
|
[TestFlight passCheckpoint:MPCheckpointSignedIn];
|
||||||
|
#endif
|
||||||
|
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointSignedIn
|
||||||
|
attributes:nil];
|
||||||
|
|
||||||
[self updateKey:tryKey];
|
|
||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)updateKey:(NSData *)key {
|
|
||||||
|
|
||||||
if (self.key != key) {
|
|
||||||
self.key = key;
|
|
||||||
|
|
||||||
if (key)
|
|
||||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeySet object:self];
|
|
||||||
else
|
|
||||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyUnset object:self];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.key) {
|
|
||||||
self.keyID = keyIDForKey(self.key);
|
|
||||||
|
|
||||||
NSData *existingKeyID = [PearlKeyChain dataOfItemForQuery:keyIDQuery()];
|
|
||||||
if (![existingKeyID isEqualToData:self.keyID]) {
|
|
||||||
inf(@"Updating key ID in keychain.");
|
|
||||||
[PearlKeyChain addOrUpdateItemForQuery:keyIDQuery()
|
|
||||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
|
||||||
self.keyID, (__bridge id)kSecValueData,
|
|
||||||
#if TARGET_OS_IPHONE
|
|
||||||
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
|
|
||||||
#endif
|
|
||||||
nil]];
|
|
||||||
}
|
|
||||||
if ([[MPConfig get].saveKey boolValue]) {
|
|
||||||
NSData *existingKey = [PearlKeyChain dataOfItemForQuery:keyQuery()];
|
|
||||||
if (![existingKey isEqualToData:self.key]) {
|
|
||||||
inf(@"Updating key in keychain.");
|
|
||||||
[PearlKeyChain addOrUpdateItemForQuery:keyQuery()
|
|
||||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
|
||||||
self.key, (__bridge id)kSecValueData,
|
|
||||||
#if TARGET_OS_IPHONE
|
|
||||||
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
|
|
||||||
#endif
|
|
||||||
nil]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef TESTFLIGHT_SDK_VERSION
|
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointSetKey];
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSData *)keyWithLength:(NSUInteger)keyLength {
|
|
||||||
|
|
||||||
return [self.key subdataWithRange:NSMakeRange(0, MIN(keyLength, self.key.length))];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -6,14 +6,17 @@
|
|||||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
#import "MPEntities.h"
|
||||||
|
|
||||||
#if TARGET_OS_IPHONE
|
#if TARGET_OS_IPHONE
|
||||||
|
|
||||||
@interface MPAppDelegate_Shared : PearlAppDelegate
|
@interface MPAppDelegate_Shared : PearlAppDelegate
|
||||||
#else
|
#else
|
||||||
@interface MPAppDelegate_Shared : NSObject <PearlConfigDelegate>
|
@interface MPAppDelegate_Shared : NSObject <PearlConfigDelegate>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@property (strong, nonatomic) NSData *key;
|
@property (strong, nonatomic) MPUserEntity *activeUser;
|
||||||
@property (strong, nonatomic) NSData *keyID;
|
@property (strong, nonatomic) MPKey *key;
|
||||||
|
|
||||||
+ (MPAppDelegate_Shared *)get;
|
+ (MPAppDelegate_Shared *)get;
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
@implementation MPAppDelegate_Shared
|
@implementation MPAppDelegate_Shared
|
||||||
|
|
||||||
@synthesize key;
|
@synthesize key;
|
||||||
@synthesize keyID;
|
@synthesize activeUser;
|
||||||
|
|
||||||
+ (MPAppDelegate_Shared *)get {
|
+ (MPAppDelegate_Shared *)get {
|
||||||
|
|
||||||
|
|||||||
@@ -18,16 +18,15 @@ typedef enum {
|
|||||||
MPImportResultInternalError,
|
MPImportResultInternalError,
|
||||||
} MPImportResult;
|
} MPImportResult;
|
||||||
|
|
||||||
@interface MPAppDelegate_Shared (Store) <UbiquityStoreManagerDelegate>
|
@interface MPAppDelegate_Shared (Store)<UbiquityStoreManagerDelegate>
|
||||||
|
|
||||||
+ (NSManagedObjectContext *)managedObjectContext;
|
+ (NSManagedObjectContext *)managedObjectContextIfReady;
|
||||||
+ (NSManagedObjectModel *)managedObjectModel;
|
+ (NSManagedObjectModel *)managedObjectModel;
|
||||||
- (NSManagedObjectContext *)managedObjectContext;
|
- (NSManagedObjectContext *)managedObjectContextIfReady;
|
||||||
- (NSManagedObjectModel *)managedObjectModel;
|
- (NSManagedObjectModel *)managedObjectModel;
|
||||||
|
|
||||||
- (UbiquityStoreManager *)storeManager;
|
- (UbiquityStoreManager *)storeManager;
|
||||||
- (void)saveContext;
|
- (void)saveContext;
|
||||||
- (void)printStore;
|
|
||||||
|
|
||||||
- (MPImportResult)importSites:(NSString *)importedSitesString withPassword:(NSString *)password
|
- (MPImportResult)importSites:(NSString *)importedSitesString withPassword:(NSString *)password
|
||||||
askConfirmation:(BOOL(^)(NSUInteger importCount, NSUInteger deleteCount))confirmation;
|
askConfirmation:(BOOL(^)(NSUInteger importCount, NSUInteger deleteCount))confirmation;
|
||||||
|
|||||||
@@ -7,18 +7,15 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "MPAppDelegate_Store.h"
|
#import "MPAppDelegate_Store.h"
|
||||||
#import "MPElementEntity.h"
|
#import "LocalyticsSession.h"
|
||||||
#import "MPConfig.h"
|
|
||||||
|
|
||||||
@implementation MPAppDelegate_Shared (Store)
|
@implementation MPAppDelegate_Shared (Store)
|
||||||
|
|
||||||
static NSDateFormatter *rfc3339DateFormatter = nil;
|
|
||||||
|
|
||||||
#pragma mark - Core Data setup
|
#pragma mark - Core Data setup
|
||||||
|
|
||||||
+ (NSManagedObjectContext *)managedObjectContext {
|
+ (NSManagedObjectContext *)managedObjectContextIfReady {
|
||||||
|
|
||||||
return [[self get] managedObjectContext];
|
return [[self get] managedObjectContextIfReady];
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (NSManagedObjectModel *)managedObjectModel {
|
+ (NSManagedObjectModel *)managedObjectModel {
|
||||||
@@ -36,36 +33,25 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
|||||||
return managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
|
return managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSManagedObjectContext *)managedObjectContext {
|
- (NSManagedObjectContext *)managedObjectContextIfReady {
|
||||||
|
|
||||||
|
if (![self storeManager].isReady)
|
||||||
|
return nil;
|
||||||
|
|
||||||
static NSManagedObjectContext *managedObjectContext = nil;
|
static NSManagedObjectContext *managedObjectContext = nil;
|
||||||
if (managedObjectContext)
|
if (managedObjectContext)
|
||||||
return managedObjectContext;
|
return managedObjectContext;
|
||||||
|
|
||||||
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
|
|
||||||
assert(coordinator);
|
|
||||||
|
|
||||||
managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
|
managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
|
||||||
[managedObjectContext performBlockAndWait:^{
|
[managedObjectContext performBlockAndWait:^{
|
||||||
managedObjectContext.persistentStoreCoordinator = coordinator;
|
managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
|
||||||
managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
|
managedObjectContext.persistentStoreCoordinator = [self storeManager].persistentStoreCoordinator;
|
||||||
|
managedObjectContext.undoManager = [NSUndoManager new];
|
||||||
}];
|
}];
|
||||||
|
|
||||||
return managedObjectContext;
|
return managedObjectContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
|
|
||||||
|
|
||||||
// Wait until the storeManager is ready.
|
|
||||||
for(__block BOOL isReady = [self storeManager].isReady; !isReady;)
|
|
||||||
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
||||||
isReady = [self storeManager].isReady;
|
|
||||||
});
|
|
||||||
|
|
||||||
assert([self storeManager].isReady);
|
|
||||||
return [self storeManager].persistentStoreCoordinator;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (UbiquityStoreManager *)storeManager {
|
- (UbiquityStoreManager *)storeManager {
|
||||||
|
|
||||||
static UbiquityStoreManager *storeManager = nil;
|
static UbiquityStoreManager *storeManager = nil;
|
||||||
@@ -76,11 +62,12 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
|||||||
localStoreURL:[[self applicationFilesDirectory] URLByAppendingPathComponent:@"MasterPassword.sqlite"]
|
localStoreURL:[[self applicationFilesDirectory] URLByAppendingPathComponent:@"MasterPassword.sqlite"]
|
||||||
containerIdentifier:@"HL3Q45LX9N.com.lyndir.lhunath.MasterPassword.shared"
|
containerIdentifier:@"HL3Q45LX9N.com.lyndir.lhunath.MasterPassword.shared"
|
||||||
#if TARGET_OS_IPHONE
|
#if TARGET_OS_IPHONE
|
||||||
additionalStoreOptions:[NSDictionary dictionaryWithObject:NSFileProtectionComplete forKey:NSPersistentStoreFileProtectionKey]
|
additionalStoreOptions:[NSDictionary dictionaryWithObject:NSFileProtectionComplete
|
||||||
|
forKey:NSPersistentStoreFileProtectionKey]
|
||||||
#else
|
#else
|
||||||
additionalStoreOptions:nil
|
additionalStoreOptions:nil
|
||||||
#endif
|
#endif
|
||||||
];
|
];
|
||||||
storeManager.delegate = self;
|
storeManager.delegate = self;
|
||||||
#ifdef DEBUG
|
#ifdef DEBUG
|
||||||
storeManager.hardResetEnabled = YES;
|
storeManager.hardResetEnabled = YES;
|
||||||
@@ -117,56 +104,11 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
|||||||
|
|
||||||
- (void)saveContext {
|
- (void)saveContext {
|
||||||
|
|
||||||
[self.managedObjectContext performBlock:^{
|
[self.managedObjectContextIfReady performBlock:^{
|
||||||
NSError *error = nil;
|
NSError *error = nil;
|
||||||
if ([self.managedObjectContext hasChanges])
|
if ([self.managedObjectContextIfReady hasChanges])
|
||||||
if (![self.managedObjectContext save:&error])
|
if (![self.managedObjectContextIfReady save:&error])
|
||||||
err(@"While saving context: %@", error);
|
err(@"While saving context: %@", error);
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)printStore {
|
|
||||||
|
|
||||||
if (![self managedObjectModel] || ![self managedObjectContext]) {
|
|
||||||
trc(@"Not printing store: store not initialized.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
[self.managedObjectContext performBlock:^{
|
|
||||||
trc(@"=== All entities ===");
|
|
||||||
for(NSEntityDescription *entity in [[self managedObjectModel] entities]) {
|
|
||||||
NSFetchRequest *request = [NSFetchRequest new];
|
|
||||||
[request setEntity:entity];
|
|
||||||
NSError *error;
|
|
||||||
NSArray *results = [[self managedObjectContext] executeFetchRequest:request error:&error];
|
|
||||||
for(NSManagedObject *o in results) {
|
|
||||||
if ([o isKindOfClass:[MPElementEntity class]]) {
|
|
||||||
MPElementEntity *e = (MPElementEntity *)o;
|
|
||||||
trc(@"For descriptor: %@, found: %@: %@ (%@)", entity.name, [o class], e.name, e.keyID);
|
|
||||||
} else {
|
|
||||||
trc(@"For descriptor: %@, found: %@", entity.name, [o class]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trc(@"---");
|
|
||||||
if ([MPAppDelegate_Shared get].keyID) {
|
|
||||||
trc(@"=== Known sites ===");
|
|
||||||
NSFetchRequest *fetchRequest = [[self managedObjectModel]
|
|
||||||
fetchRequestFromTemplateWithName:@"MPElements"
|
|
||||||
substitutionVariables:[NSDictionary dictionaryWithObjectsAndKeys:
|
|
||||||
@"", @"query",
|
|
||||||
[MPAppDelegate_Shared get].keyID, @"keyID",
|
|
||||||
nil]];
|
|
||||||
[fetchRequest setSortDescriptors:
|
|
||||||
[NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]]];
|
|
||||||
|
|
||||||
NSError *error = nil;
|
|
||||||
for (MPElementEntity *e in [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]) {
|
|
||||||
trc(@"Found site: %@ (%@): %@", e.name, e.keyID, e);
|
|
||||||
}
|
|
||||||
trc(@"---");
|
|
||||||
} else
|
|
||||||
trc(@"Not printing sites: master password not set.");
|
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +116,7 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
|||||||
|
|
||||||
- (NSManagedObjectContext *)managedObjectContextForUbiquityStoreManager:(UbiquityStoreManager *)usm {
|
- (NSManagedObjectContext *)managedObjectContextForUbiquityStoreManager:(UbiquityStoreManager *)usm {
|
||||||
|
|
||||||
return self.managedObjectContext;
|
return self.managedObjectContextIfReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message {
|
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message {
|
||||||
@@ -184,23 +126,28 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
|||||||
|
|
||||||
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToiCloud:(BOOL)iCloudEnabled {
|
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToiCloud:(BOOL)iCloudEnabled {
|
||||||
|
|
||||||
// manager.iCloudEnabled is more reliable (eg. iOS tampers with didSwitch a bit)
|
// manager.iCloudEnabled is more reliable (eg. iOS' MPAppDelegate tampers with didSwitch a bit)
|
||||||
iCloudEnabled = manager.iCloudEnabled;
|
iCloudEnabled = manager.iCloudEnabled;
|
||||||
|
inf(@"Using iCloud? %@", iCloudEnabled? @"YES": @"NO");
|
||||||
|
|
||||||
#ifdef TESTFLIGHT_SDK_VERSION
|
#ifdef TESTFLIGHT_SDK_VERSION
|
||||||
[TestFlight passCheckpoint:iCloudEnabled? MPTestFlightCheckpointCloudEnabled: MPTestFlightCheckpointCloudDisabled];
|
[TestFlight passCheckpoint:iCloudEnabled? MPCheckpointCloudEnabled: MPCheckpointCloudDisabled];
|
||||||
#endif
|
#endif
|
||||||
|
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointCloud
|
||||||
|
attributes:[NSDictionary dictionaryWithObject:iCloudEnabled? @"YES": @"NO"
|
||||||
|
forKey:@"enabled"]];
|
||||||
|
|
||||||
inf(@"Using iCloud? %@", iCloudEnabled? @"YES": @"NO");
|
|
||||||
[MPConfig get].iCloud = [NSNumber numberWithBool:iCloudEnabled];
|
[MPConfig get].iCloud = [NSNumber numberWithBool:iCloudEnabled];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause context:(id)context {
|
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didEncounterError:(NSError *)error cause:(UbiquityStoreManagerErrorCause)cause
|
||||||
|
context:(id)context {
|
||||||
|
|
||||||
|
err(@"StoreManager: cause=%d, context=%@, error=%@", cause, context, error);
|
||||||
|
|
||||||
#ifdef TESTFLIGHT_SDK_VERSION
|
#ifdef TESTFLIGHT_SDK_VERSION
|
||||||
[TestFlight passCheckpoint:PearlString(@"MPTestFlightCheckpointMPErrorUbiquity_%d", cause)];
|
[TestFlight passCheckpoint:PearlString(@"MPCheckpointMPErrorUbiquity_%d", cause)];
|
||||||
#endif
|
#endif
|
||||||
err(@"StoreManager: cause=%d, context=%@, error=%@", cause, context, error);
|
|
||||||
|
|
||||||
switch (cause) {
|
switch (cause) {
|
||||||
case UbiquityStoreManagerErrorCauseDeleteStore:
|
case UbiquityStoreManagerErrorCauseDeleteStore:
|
||||||
@@ -209,21 +156,24 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
|||||||
case UbiquityStoreManagerErrorCauseClearStore:
|
case UbiquityStoreManagerErrorCauseClearStore:
|
||||||
break;
|
break;
|
||||||
case UbiquityStoreManagerErrorCauseOpenLocalStore: {
|
case UbiquityStoreManagerErrorCauseOpenLocalStore: {
|
||||||
#ifdef TESTFLIGHT_SDK_VERSION
|
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointLocalStoreIncompatible];
|
|
||||||
#endif
|
|
||||||
wrn(@"Local store could not be opened, resetting it.");
|
wrn(@"Local store could not be opened, resetting it.");
|
||||||
|
|
||||||
|
#ifdef TESTFLIGHT_SDK_VERSION
|
||||||
|
[TestFlight passCheckpoint:MPCheckpointLocalStoreIncompatible];
|
||||||
|
#endif
|
||||||
|
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointLocalStoreIncompatible attributes:nil];
|
||||||
manager.hardResetEnabled = YES;
|
manager.hardResetEnabled = YES;
|
||||||
[manager hardResetLocalStorage];
|
[manager hardResetLocalStorage];
|
||||||
|
|
||||||
[NSException raise:NSGenericException format:@"Local store was reset, application must be restarted to use it."];
|
Throw(@"Local store was reset, application must be restarted to use it.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
case UbiquityStoreManagerErrorCauseOpenCloudStore: {
|
case UbiquityStoreManagerErrorCauseOpenCloudStore: {
|
||||||
#ifdef TESTFLIGHT_SDK_VERSION
|
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointCloudStoreIncompatible];
|
|
||||||
#endif
|
|
||||||
wrn(@"iCloud store could not be opened, resetting it.");
|
wrn(@"iCloud store could not be opened, resetting it.");
|
||||||
|
|
||||||
|
#ifdef TESTFLIGHT_SDK_VERSION
|
||||||
|
[TestFlight passCheckpoint:MPCheckpointCloudStoreIncompatible];
|
||||||
|
#endif
|
||||||
|
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointCloudStoreIncompatible attributes:nil];
|
||||||
manager.hardResetEnabled = YES;
|
manager.hardResetEnabled = YES;
|
||||||
[manager hardResetCloudStorage];
|
[manager hardResetCloudStorage];
|
||||||
break;
|
break;
|
||||||
@@ -233,49 +183,37 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
|||||||
|
|
||||||
#pragma mark - Import / Export
|
#pragma mark - Import / Export
|
||||||
|
|
||||||
- (void)loadRFC3339DateFormatter {
|
|
||||||
|
|
||||||
if (rfc3339DateFormatter)
|
|
||||||
return;
|
|
||||||
|
|
||||||
rfc3339DateFormatter = [NSDateFormatter new];
|
|
||||||
NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
|
|
||||||
[rfc3339DateFormatter setLocale:enUSPOSIXLocale];
|
|
||||||
[rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
|
|
||||||
[rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (MPImportResult)importSites:(NSString *)importedSitesString withPassword:(NSString *)password
|
- (MPImportResult)importSites:(NSString *)importedSitesString withPassword:(NSString *)password
|
||||||
askConfirmation:(BOOL(^)(NSUInteger importCount, NSUInteger deleteCount))confirmation {
|
askConfirmation:(BOOL(^)(NSUInteger importCount, NSUInteger deleteCount))confirmation {
|
||||||
|
|
||||||
[self loadRFC3339DateFormatter];
|
inf(@"Importing sites.");
|
||||||
|
|
||||||
static NSRegularExpression *headerPattern, *sitePattern;
|
static NSRegularExpression *headerPattern, *sitePattern;
|
||||||
__autoreleasing NSError *error;
|
__block NSError *error = nil;
|
||||||
if (!headerPattern) {
|
if (!headerPattern) {
|
||||||
headerPattern = [[NSRegularExpression alloc]
|
headerPattern = [[NSRegularExpression alloc] initWithPattern:@"^#[[:space:]]*([^:]+): (.*)"
|
||||||
initWithPattern:@"^#[[:space:]]*([^:]+): (.*)"
|
options:0 error:&error];
|
||||||
options:0 error:&error];
|
|
||||||
if (error)
|
if (error)
|
||||||
err(@"Error loading the header pattern: %@", error);
|
err(@"Error loading the header pattern: %@", error);
|
||||||
}
|
}
|
||||||
if (!sitePattern) {
|
if (!sitePattern) {
|
||||||
sitePattern = [[NSRegularExpression alloc]
|
sitePattern = [[NSRegularExpression alloc] initWithPattern:@"^([^[:space:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([[:digit:]]+)(:[[:digit:]]+)?[[:space:]]+([^\t]+)\t(.*)"
|
||||||
initWithPattern:@"^([^[:space:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([^\t]+)\t(.*)"
|
options:0 error:&error];
|
||||||
options:0 error:&error];
|
|
||||||
if (error)
|
if (error)
|
||||||
err(@"Error loading the site pattern: %@", error);
|
err(@"Error loading the site pattern: %@", error);
|
||||||
}
|
}
|
||||||
if (!headerPattern || !sitePattern)
|
if (!headerPattern || !sitePattern)
|
||||||
return MPImportResultInternalError;
|
return MPImportResultInternalError;
|
||||||
|
|
||||||
NSString *keyIDHex = nil;
|
MPKey *key = nil;
|
||||||
BOOL headerStarted = NO, headerEnded = NO;
|
__block MPUserEntity *user = nil;
|
||||||
NSArray *importedSiteLines = [importedSitesString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
|
NSString *bundleVersion = nil, *keyIDHex = nil, *userName = nil;
|
||||||
NSMutableSet *elementsToDelete = [NSMutableSet set];
|
BOOL headerStarted = NO, headerEnded = NO, clearText = NO;
|
||||||
|
NSArray *importedSiteLines = [importedSitesString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
|
||||||
|
NSMutableSet *elementsToDelete = [NSMutableSet set];
|
||||||
NSMutableArray *importedSiteElements = [NSMutableArray arrayWithCapacity:[importedSiteLines count]];
|
NSMutableArray *importedSiteElements = [NSMutableArray arrayWithCapacity:[importedSiteLines count]];
|
||||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
||||||
for(NSString *importedSiteLine in importedSiteLines) {
|
for (NSString *importedSiteLine in importedSiteLines) {
|
||||||
if ([importedSiteLine hasPrefix:@"#"]) {
|
if ([importedSiteLine hasPrefix:@"#"]) {
|
||||||
// Comment or header
|
// Comment or header
|
||||||
if (!headerStarted) {
|
if (!headerStarted) {
|
||||||
@@ -295,20 +233,51 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
|||||||
err(@"Invalid header format in line: %@", importedSiteLine);
|
err(@"Invalid header format in line: %@", importedSiteLine);
|
||||||
return MPImportResultMalformedInput;
|
return MPImportResultMalformedInput;
|
||||||
}
|
}
|
||||||
NSTextCheckingResult *headerElements = [[headerPattern matchesInString:importedSiteLine options:0 range:NSMakeRange(0, [importedSiteLine length])] lastObject];
|
NSTextCheckingResult *headerElements = [[headerPattern matchesInString:importedSiteLine options:0
|
||||||
NSString *key = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:1]];
|
range:NSMakeRange(0, [importedSiteLine length])] lastObject];
|
||||||
NSString *value = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:2]];
|
NSString *headerName = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:1]];
|
||||||
if ([key isEqualToString:@"Key ID"]) {
|
NSString *headerValue = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:2]];
|
||||||
if (![(keyIDHex = value) isEqualToString:[keyIDForPassword(password) encodeHex]])
|
if ([headerName isEqualToString:@"User Name"]) {
|
||||||
return MPImportResultInvalidPassword;
|
userName = headerValue;
|
||||||
|
|
||||||
|
NSFetchRequest *userFetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPUserEntity class])];
|
||||||
|
userFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", userName];
|
||||||
|
__block NSArray *users = nil;
|
||||||
|
[self.managedObjectContextIfReady performBlockAndWait:^{
|
||||||
|
users = [self.managedObjectContextIfReady executeFetchRequest:userFetchRequest error:&error];
|
||||||
|
}];
|
||||||
|
if (!users) {
|
||||||
|
err(@"While looking for user: %@, error: %@", userName, error);
|
||||||
|
return MPImportResultInternalError;
|
||||||
|
}
|
||||||
|
if ([users count] > 1) {
|
||||||
|
err(@"While looking for user: %@, found more than one: %u", userName, [users count]);
|
||||||
|
return MPImportResultInternalError;
|
||||||
|
}
|
||||||
|
|
||||||
|
user = [users count]? [users lastObject]: nil;
|
||||||
|
dbg(@"Found user: %@", [user debugDescription]);
|
||||||
|
}
|
||||||
|
if ([headerName isEqualToString:@"Key ID"])
|
||||||
|
keyIDHex = headerValue;
|
||||||
|
if ([headerName isEqualToString:@"Version"])
|
||||||
|
bundleVersion = headerValue;
|
||||||
|
if ([headerName isEqualToString:@"Passwords"]) {
|
||||||
|
if ([headerValue isEqualToString:@"VISIBLE"])
|
||||||
|
clearText = YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!headerEnded)
|
if (!headerEnded)
|
||||||
continue;
|
continue;
|
||||||
if (!keyIDHex)
|
if (!keyIDHex || ![userName length])
|
||||||
return MPImportResultMalformedInput;
|
return MPImportResultMalformedInput;
|
||||||
|
if (!key) {
|
||||||
|
key = [MPAlgorithmDefaultForBundleVersion(bundleVersion) keyForPassword:password ofUserNamed:userName];
|
||||||
|
if (![keyIDHex isEqualToString:[key.keyID encodeHex]])
|
||||||
|
return MPImportResultInvalidPassword;
|
||||||
|
}
|
||||||
if (![importedSiteLine length])
|
if (![importedSiteLine length])
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -317,68 +286,116 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
|||||||
err(@"Invalid site format in line: %@", importedSiteLine);
|
err(@"Invalid site format in line: %@", importedSiteLine);
|
||||||
return MPImportResultMalformedInput;
|
return MPImportResultMalformedInput;
|
||||||
}
|
}
|
||||||
NSTextCheckingResult *siteElements = [[sitePattern matchesInString:importedSiteLine options:0 range:NSMakeRange(0, [importedSiteLine length])] lastObject];
|
NSTextCheckingResult *siteElements = [[sitePattern matchesInString:importedSiteLine options:0
|
||||||
NSString *lastUsed = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:1]];
|
range:NSMakeRange(0, [importedSiteLine length])] lastObject];
|
||||||
NSString *uses = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:2]];
|
NSString *lastUsed = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:1]];
|
||||||
NSString *type = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:3]];
|
NSString *uses = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:2]];
|
||||||
NSString *name = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:4]];
|
NSString *type = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:3]];
|
||||||
NSString *exportContent = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:5]];
|
NSString *version = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:4]];
|
||||||
|
NSString *name = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:5]];
|
||||||
|
NSString *exportContent = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:6]];
|
||||||
|
|
||||||
// Find existing site.
|
// Find existing site.
|
||||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND keyID == %@", name, keyIDHex];
|
if (user) {
|
||||||
NSArray *existingSites = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
|
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND user == %@", name, user];
|
||||||
if (error)
|
__block NSArray *existingSites = nil;
|
||||||
err(@"Couldn't search existing sites: %@", error);
|
[self.managedObjectContextIfReady performBlockAndWait:^{
|
||||||
if (!existingSites)
|
existingSites = [self.managedObjectContextIfReady executeFetchRequest:fetchRequest error:&error];
|
||||||
return MPImportResultInternalError;
|
}];
|
||||||
|
if (!existingSites) {
|
||||||
|
err(@"Lookup of existing sites failed for site: %@, user: %@, error: %@", name, user.userID, error);
|
||||||
|
return MPImportResultInternalError;
|
||||||
|
} else
|
||||||
|
if (existingSites.count)
|
||||||
|
dbg(@"Existing sites: %@", existingSites);
|
||||||
|
|
||||||
[elementsToDelete addObjectsFromArray:existingSites];
|
[elementsToDelete addObjectsFromArray:existingSites];
|
||||||
[importedSiteElements addObject:[NSArray arrayWithObjects:lastUsed, uses, type, name, exportContent, nil]];
|
[importedSiteElements addObject:[NSArray arrayWithObjects:lastUsed, uses, type, version, name, exportContent, nil]];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inf(@"Importing %u sites, deleting %u sites, for user: %@",
|
||||||
|
[importedSiteElements count], [elementsToDelete count], [MPUserEntity idFor:userName]);
|
||||||
|
|
||||||
// Ask for confirmation to import these sites.
|
// Ask for confirmation to import these sites.
|
||||||
if (!confirmation([importedSiteElements count], [elementsToDelete count]))
|
if (!confirmation([importedSiteElements count], [elementsToDelete count])) {
|
||||||
|
inf(@"Import cancelled.");
|
||||||
return MPImportResultCancelled;
|
return MPImportResultCancelled;
|
||||||
|
|
||||||
// Delete existing sites.
|
|
||||||
[elementsToDelete enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
|
|
||||||
inf(@"Deleting site: %@, it will be replaced by an imported site.", [obj name]);
|
|
||||||
[self.managedObjectContext deleteObject:obj];
|
|
||||||
}];
|
|
||||||
[self saveContext];
|
|
||||||
|
|
||||||
// Import new sites.
|
|
||||||
for (NSArray *siteElements in importedSiteElements) {
|
|
||||||
NSDate *lastUsed = [rfc3339DateFormatter dateFromString:[siteElements objectAtIndex:0]];
|
|
||||||
NSInteger uses = [[siteElements objectAtIndex:1] integerValue];
|
|
||||||
MPElementType type = (unsigned)[[siteElements objectAtIndex:2] integerValue];
|
|
||||||
NSString *name = [siteElements objectAtIndex:3];
|
|
||||||
NSString *exportContent = [siteElements objectAtIndex:4];
|
|
||||||
|
|
||||||
// Create new site.
|
|
||||||
inf(@"Importing site: name=%@, lastUsed=%@, uses=%d, type=%u, keyID=%@", name, lastUsed, uses, type, keyIDHex);
|
|
||||||
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:ClassNameFromMPElementType(type)
|
|
||||||
inManagedObjectContext:self.managedObjectContext];
|
|
||||||
element.name = name;
|
|
||||||
element.keyID = [keyIDHex decodeHex];
|
|
||||||
element.type = type;
|
|
||||||
element.uses = uses;
|
|
||||||
element.lastUsed = [lastUsed timeIntervalSinceReferenceDate];
|
|
||||||
if ([exportContent length])
|
|
||||||
[element importContent:exportContent];
|
|
||||||
}
|
}
|
||||||
[self saveContext];
|
|
||||||
|
|
||||||
|
BOOL success = NO;
|
||||||
|
[self.managedObjectContextIfReady.undoManager beginUndoGrouping];
|
||||||
|
@try {
|
||||||
|
|
||||||
|
// Delete existing sites.
|
||||||
|
if (elementsToDelete.count)
|
||||||
|
[self.managedObjectContextIfReady performBlockAndWait:^{
|
||||||
|
[elementsToDelete enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
|
||||||
|
inf(@"Deleting site: %@, it will be replaced by an imported site.", [obj name]);
|
||||||
|
dbg(@"Deleted Element: %@", [obj debugDescription]);
|
||||||
|
[self.managedObjectContextIfReady deleteObject:obj];
|
||||||
|
}];
|
||||||
|
}];
|
||||||
|
|
||||||
|
// Import new sites.
|
||||||
|
if (!user) {
|
||||||
|
[self.managedObjectContextIfReady performBlockAndWait:^{
|
||||||
|
user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPUserEntity class])
|
||||||
|
inManagedObjectContext:self.managedObjectContextIfReady];
|
||||||
|
user.name = userName;
|
||||||
|
user.keyID = [keyIDHex decodeHex];
|
||||||
|
}];
|
||||||
|
dbg(@"Created User: %@", [user debugDescription]);
|
||||||
|
}
|
||||||
|
for (NSArray *siteElements in importedSiteElements) {
|
||||||
|
NSDate *lastUsed = [[NSDateFormatter rfc3339DateFormatter] dateFromString:[siteElements objectAtIndex:0]];
|
||||||
|
NSUInteger uses = (unsigned)[[siteElements objectAtIndex:1] integerValue];
|
||||||
|
MPElementType type = (MPElementType)[[siteElements objectAtIndex:2] integerValue];
|
||||||
|
NSUInteger version = (unsigned)[[siteElements objectAtIndex:3] integerValue];
|
||||||
|
NSString *name = [siteElements objectAtIndex:4];
|
||||||
|
NSString *exportContent = [siteElements objectAtIndex:5];
|
||||||
|
|
||||||
|
// Create new site.
|
||||||
|
[self.managedObjectContextIfReady performBlockAndWait:^{
|
||||||
|
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:[key.algorithm classNameOfType:type]
|
||||||
|
inManagedObjectContext:self.managedObjectContextIfReady];
|
||||||
|
element.name = name;
|
||||||
|
element.user = user;
|
||||||
|
element.type = type;
|
||||||
|
element.uses = uses;
|
||||||
|
element.lastUsed = lastUsed;
|
||||||
|
element.version = version;
|
||||||
|
if ([exportContent length]) {
|
||||||
|
if (clearText)
|
||||||
|
[element importClearTextContent:exportContent usingKey:key];
|
||||||
|
else
|
||||||
|
[element importProtectedContent:exportContent];
|
||||||
|
}
|
||||||
|
dbg(@"Created Element: %@", [element debugDescription]);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
[self saveContext];
|
||||||
|
success = YES;
|
||||||
|
inf(@"Import completed successfully.");
|
||||||
#ifdef TESTFLIGHT_SDK_VERSION
|
#ifdef TESTFLIGHT_SDK_VERSION
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointSitesImported];
|
[TestFlight passCheckpoint:MPCheckpointSitesImported];
|
||||||
#endif
|
#endif
|
||||||
|
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointSitesImported attributes:nil];
|
||||||
|
|
||||||
return MPImportResultSuccess;
|
return MPImportResultSuccess;
|
||||||
|
}
|
||||||
|
@finally {
|
||||||
|
[self.managedObjectContextIfReady.undoManager endUndoGrouping];
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
[self.managedObjectContextIfReady.undoManager undoNestedGroup];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords {
|
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords {
|
||||||
|
|
||||||
[self loadRFC3339DateFormatter];
|
inf(@"Exporting sites, %@, for: %@", showPasswords? @"showing passwords": @"omitting passwords", self.activeUser.userID);
|
||||||
|
|
||||||
// Header.
|
// Header.
|
||||||
NSMutableString *export = [NSMutableString new];
|
NSMutableString *export = [NSMutableString new];
|
||||||
@@ -390,8 +407,9 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
|||||||
[export appendFormat:@"# \n"];
|
[export appendFormat:@"# \n"];
|
||||||
[export appendFormat:@"##\n"];
|
[export appendFormat:@"##\n"];
|
||||||
[export appendFormat:@"# Version: %@\n", [PearlInfoPlist get].CFBundleVersion];
|
[export appendFormat:@"# Version: %@\n", [PearlInfoPlist get].CFBundleVersion];
|
||||||
[export appendFormat:@"# Key ID: %@\n", [self.keyID encodeHex]];
|
[export appendFormat:@"# User Name: %@\n", self.activeUser.name];
|
||||||
[export appendFormat:@"# Date: %@\n", [rfc3339DateFormatter stringFromDate:[NSDate date]]];
|
[export appendFormat:@"# Key ID: %@\n", [self.activeUser.keyID encodeHex]];
|
||||||
|
[export appendFormat:@"# Date: %@\n", [[NSDateFormatter rfc3339DateFormatter] stringFromDate:[NSDate date]]];
|
||||||
if (showPasswords)
|
if (showPasswords)
|
||||||
[export appendFormat:@"# Passwords: VISIBLE\n"];
|
[export appendFormat:@"# Passwords: VISIBLE\n"];
|
||||||
else
|
else
|
||||||
@@ -402,36 +420,33 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
|||||||
[export appendFormat:@"# used used type name\tpassword\n"];
|
[export appendFormat:@"# used used type name\tpassword\n"];
|
||||||
|
|
||||||
// Sites.
|
// Sites.
|
||||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
for (MPElementEntity *element in self.activeUser.elements) {
|
||||||
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]];
|
NSDate *lastUsed = element.lastUsed;
|
||||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"keyID == %@", self.keyID];
|
NSUInteger uses = element.uses;
|
||||||
__autoreleasing NSError *error = nil;
|
MPElementType type = element.type;
|
||||||
NSArray *elements = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
|
NSUInteger version = element.version;
|
||||||
if (error)
|
NSString *name = element.name;
|
||||||
err(@"Error fetching sites for export: %@", error);
|
|
||||||
|
|
||||||
for (MPElementEntity *element in elements) {
|
|
||||||
NSTimeInterval lastUsed = element.lastUsed;
|
|
||||||
int16_t uses = element.uses;
|
|
||||||
MPElementType type = (unsigned)element.type;
|
|
||||||
NSString *name = element.name;
|
|
||||||
NSString *content = nil;
|
NSString *content = nil;
|
||||||
|
|
||||||
// Determine the content to export.
|
// Determine the content to export.
|
||||||
if (!(type & MPElementFeatureDevicePrivate)) {
|
if (!(type & MPElementFeatureDevicePrivate)) {
|
||||||
if (showPasswords)
|
if (showPasswords)
|
||||||
content = element.content;
|
content = element.content;
|
||||||
else if (type & MPElementFeatureExportContent)
|
else
|
||||||
content = element.exportContent;
|
if (type & MPElementFeatureExportContent)
|
||||||
|
content = element.exportContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
[export appendFormat:@"%@ %8d %8d %20s\t%@\n",
|
[export appendFormat:@"%@ %8d %8s %20s\t%@\n",
|
||||||
[rfc3339DateFormatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:lastUsed]], uses, type, [name cStringUsingEncoding:NSUTF8StringEncoding], content? content: @""];
|
[[NSDateFormatter rfc3339DateFormatter] stringFromDate:lastUsed], uses,
|
||||||
|
[PearlString(@"%u:%u", type, version) UTF8String], [name UTF8String], content
|
||||||
|
? content: @""];
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef TESTFLIGHT_SDK_VERSION
|
#ifdef TESTFLIGHT_SDK_VERSION
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointSitesExported];
|
[TestFlight passCheckpoint:MPCheckpointSitesExported];
|
||||||
#endif
|
#endif
|
||||||
|
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointSitesExported attributes:nil];
|
||||||
|
|
||||||
return export;
|
return export;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,11 @@
|
|||||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
#import "Pearl.h"
|
||||||
|
|
||||||
@interface MPConfig : PearlConfig
|
@interface MPConfig : PearlConfig
|
||||||
|
|
||||||
@property (nonatomic, retain) NSNumber *saveKey;
|
@property (nonatomic, retain) NSNumber *rememberLogin;
|
||||||
@property (nonatomic, retain) NSNumber *rememberKey;
|
|
||||||
|
|
||||||
@property (nonatomic, retain) NSNumber *iCloud;
|
@property (nonatomic, retain) NSNumber *iCloud;
|
||||||
@property (nonatomic, retain) NSNumber *iCloudDecided;
|
@property (nonatomic, retain) NSNumber *iCloudDecided;
|
||||||
|
|||||||
@@ -6,25 +6,23 @@
|
|||||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "MPConfig.h"
|
|
||||||
#import "MPAppDelegate.h"
|
#import "MPAppDelegate.h"
|
||||||
|
|
||||||
@implementation MPConfig
|
@implementation MPConfig
|
||||||
@dynamic saveKey, rememberKey, iCloud, iCloudDecided;
|
@dynamic rememberLogin, iCloud, iCloudDecided;
|
||||||
|
|
||||||
- (id)init {
|
- (id)init {
|
||||||
|
|
||||||
if(!(self = [super init]))
|
if (!(self = [super init]))
|
||||||
return nil;
|
return nil;
|
||||||
|
|
||||||
[self.defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
|
[self.defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(askForReviews)),
|
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(askForReviews)),
|
||||||
|
|
||||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(saveKey)),
|
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(rememberLogin)),
|
||||||
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(rememberKey)),
|
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(iCloud)),
|
||||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(iCloud)),
|
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(iCloudDecided)),
|
||||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(iCloudDecided)),
|
nil]];
|
||||||
nil]];
|
|
||||||
|
|
||||||
self.delegate = [MPAppDelegate get];
|
self.delegate = [MPAppDelegate get];
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
//
|
//
|
||||||
// MPElementEntity.h
|
// MPElementEntity.h
|
||||||
// MasterPassword
|
// MasterPassword-iOS
|
||||||
//
|
//
|
||||||
// Created by Maarten Billemont on 02/01/12.
|
// Created by Maarten Billemont on 17/07/12.
|
||||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#import <CoreData/CoreData.h>
|
#import <CoreData/CoreData.h>
|
||||||
|
|
||||||
|
@class MPUserEntity;
|
||||||
|
|
||||||
@interface MPElementEntity : NSManagedObject
|
@interface MPElementEntity : NSManagedObject
|
||||||
|
|
||||||
@property (nonatomic, retain) NSString *name;
|
@property (nonatomic, retain) id content;
|
||||||
@property (nonatomic, retain) NSData *keyID;
|
@property (nonatomic, retain) NSDate * lastUsed;
|
||||||
@property (nonatomic, assign) int16_t type;
|
@property (nonatomic, retain) NSString * name;
|
||||||
@property (nonatomic, assign) int16_t uses;
|
@property (nonatomic, retain) NSNumber * requiresExplicitMigration_;
|
||||||
@property (nonatomic, assign) NSTimeInterval lastUsed;
|
@property (nonatomic, retain) NSNumber * type_;
|
||||||
|
@property (nonatomic, retain) NSString * userName;
|
||||||
@property (nonatomic, retain, readonly) id content;
|
@property (nonatomic, retain) NSNumber * uses_;
|
||||||
|
@property (nonatomic, retain) NSNumber * version_;
|
||||||
- (int16_t)use;
|
@property (nonatomic, retain) MPUserEntity *user;
|
||||||
- (NSString *)exportContent;
|
|
||||||
- (void)importContent:(NSString *)content;
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -1,51 +1,25 @@
|
|||||||
//
|
//
|
||||||
// MPElementEntity.m
|
// MPElementEntity.m
|
||||||
// MasterPassword
|
// MasterPassword-iOS
|
||||||
//
|
//
|
||||||
// Created by Maarten Billemont on 02/01/12.
|
// Created by Maarten Billemont on 17/07/12.
|
||||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "MPElementEntity.h"
|
#import "MPElementEntity.h"
|
||||||
|
#import "MPUserEntity.h"
|
||||||
|
|
||||||
|
|
||||||
@implementation MPElementEntity
|
@implementation MPElementEntity
|
||||||
|
|
||||||
@dynamic name;
|
@dynamic content;
|
||||||
@dynamic keyID;
|
|
||||||
@dynamic type;
|
|
||||||
@dynamic uses;
|
|
||||||
@dynamic lastUsed;
|
@dynamic lastUsed;
|
||||||
|
@dynamic name;
|
||||||
- (int16_t)use {
|
@dynamic requiresExplicitMigration_;
|
||||||
|
@dynamic type_;
|
||||||
self.lastUsed = [[NSDate date] timeIntervalSinceReferenceDate];
|
@dynamic userName;
|
||||||
return ++self.uses;
|
@dynamic uses_;
|
||||||
}
|
@dynamic version_;
|
||||||
|
@dynamic user;
|
||||||
- (id)content {
|
|
||||||
|
|
||||||
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Content implementation missing." userInfo:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSString *)exportContent {
|
|
||||||
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)importContent:(NSString *)content {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSString *)description {
|
|
||||||
|
|
||||||
return PearlString(@"%@:%@", [self class], [self name]);
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSString *)debugDescription {
|
|
||||||
|
|
||||||
return PearlString(@"{%@: name=%@, keyID=%@, type=%d, uses=%d, lastUsed=%@}",
|
|
||||||
NSStringFromClass([self class]), self.name, self.keyID, self.type, self.uses, self.lastUsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
//
|
//
|
||||||
// MPElementGeneratedEntity.h
|
// MPElementGeneratedEntity.h
|
||||||
// MasterPassword
|
// MasterPassword-iOS
|
||||||
//
|
//
|
||||||
// Created by Maarten Billemont on 16/01/12.
|
// Created by Maarten Billemont on 17/07/12.
|
||||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
@@ -13,6 +13,6 @@
|
|||||||
|
|
||||||
@interface MPElementGeneratedEntity : MPElementEntity
|
@interface MPElementGeneratedEntity : MPElementEntity
|
||||||
|
|
||||||
@property (nonatomic, assign) int32_t counter;
|
@property (nonatomic, retain) NSNumber * counter_;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -1,31 +1,16 @@
|
|||||||
//
|
//
|
||||||
// MPElementGeneratedEntity.m
|
// MPElementGeneratedEntity.m
|
||||||
// MasterPassword
|
// MasterPassword-iOS
|
||||||
//
|
//
|
||||||
// Created by Maarten Billemont on 16/01/12.
|
// Created by Maarten Billemont on 17/07/12.
|
||||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "MPElementGeneratedEntity.h"
|
#import "MPElementGeneratedEntity.h"
|
||||||
#import "MPAppDelegate.h"
|
|
||||||
#import "MPAppDelegate_Key.h"
|
|
||||||
|
|
||||||
|
|
||||||
@implementation MPElementGeneratedEntity
|
@implementation MPElementGeneratedEntity
|
||||||
|
|
||||||
@dynamic counter;
|
@dynamic counter_;
|
||||||
|
|
||||||
- (id)content {
|
|
||||||
|
|
||||||
if (!(self.type & MPElementTypeClassGenerated)) {
|
|
||||||
err(@"Corrupt element: %@, type: %d is not in MPElementTypeClassGenerated", self.name, self.type);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (![self.name length])
|
|
||||||
return nil;
|
|
||||||
|
|
||||||
return MPCalculateContent((unsigned)self.type, self.name, [MPAppDelegate get].key, self.counter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
//
|
//
|
||||||
// MPElementStoredEntity.h
|
// MPElementStoredEntity.h
|
||||||
// MasterPassword
|
// MasterPassword-iOS
|
||||||
//
|
//
|
||||||
// Created by Maarten Billemont on 02/01/12.
|
// Created by Maarten Billemont on 17/07/12.
|
||||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
@@ -13,6 +13,6 @@
|
|||||||
|
|
||||||
@interface MPElementStoredEntity : MPElementEntity
|
@interface MPElementStoredEntity : MPElementEntity
|
||||||
|
|
||||||
@property (nonatomic, retain, readwrite) id content;
|
@property (nonatomic, retain) id contentObject;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -1,76 +1,16 @@
|
|||||||
//
|
//
|
||||||
// MPElementStoredEntity.m
|
// MPElementStoredEntity.m
|
||||||
// MasterPassword
|
// MasterPassword-iOS
|
||||||
//
|
//
|
||||||
// Created by Maarten Billemont on 02/01/12.
|
// Created by Maarten Billemont on 17/07/12.
|
||||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "MPElementStoredEntity.h"
|
#import "MPElementStoredEntity.h"
|
||||||
#import "MPAppDelegate.h"
|
|
||||||
#import "MPAppDelegate_Key.h"
|
|
||||||
|
|
||||||
@interface MPElementStoredEntity ()
|
|
||||||
|
|
||||||
@property (nonatomic, retain, readwrite) id contentObject;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation MPElementStoredEntity
|
@implementation MPElementStoredEntity
|
||||||
|
|
||||||
@dynamic contentObject;
|
@dynamic contentObject;
|
||||||
|
|
||||||
+ (NSDictionary *)queryForDevicePrivateElementNamed:(NSString *)name {
|
|
||||||
|
|
||||||
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
|
||||||
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
|
||||||
@"DevicePrivate", (__bridge id)kSecAttrService,
|
|
||||||
name, (__bridge id)kSecAttrAccount,
|
|
||||||
nil]
|
|
||||||
matches:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id)content {
|
|
||||||
|
|
||||||
assert(self.type & MPElementTypeClassStored);
|
|
||||||
|
|
||||||
NSData *encryptedContent;
|
|
||||||
if (self.type & MPElementFeatureDevicePrivate)
|
|
||||||
encryptedContent = [PearlKeyChain dataOfItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]];
|
|
||||||
else
|
|
||||||
encryptedContent = self.contentObject;
|
|
||||||
|
|
||||||
NSData *decryptedContent = [encryptedContent decryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
|
|
||||||
padding:YES];
|
|
||||||
return [[NSString alloc] initWithBytes:decryptedContent.bytes length:decryptedContent.length encoding:NSUTF8StringEncoding];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setContent:(id)content {
|
|
||||||
|
|
||||||
NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
|
|
||||||
padding:YES];
|
|
||||||
|
|
||||||
if (self.type & MPElementFeatureDevicePrivate) {
|
|
||||||
[PearlKeyChain addOrUpdateItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]
|
|
||||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
|
||||||
encryptedContent, (__bridge id)kSecValueData,
|
|
||||||
#if TARGET_OS_IPHONE
|
|
||||||
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, (__bridge id)kSecAttrAccessible,
|
|
||||||
#endif
|
|
||||||
nil]];
|
|
||||||
self.contentObject = nil;
|
|
||||||
} else
|
|
||||||
self.contentObject = encryptedContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSString *)exportContent {
|
|
||||||
|
|
||||||
return [self.contentObject encodeBase64];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)importContent:(NSString *)content {
|
|
||||||
|
|
||||||
self.contentObject = [content decodeBase64];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
54
MasterPassword/MPEntities.h
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// MPElementEntities.h
|
||||||
|
// MasterPassword-iOS
|
||||||
|
//
|
||||||
|
// Created by Maarten Billemont on 31/05/12.
|
||||||
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "MPElementEntity.h"
|
||||||
|
#import "MPElementStoredEntity.h"
|
||||||
|
#import "MPElementGeneratedEntity.h"
|
||||||
|
#import "MPUserEntity.h"
|
||||||
|
#import "MPAlgorithm.h"
|
||||||
|
|
||||||
|
#define MPAvatarCount 19
|
||||||
|
|
||||||
|
@interface MPElementEntity (MP)
|
||||||
|
|
||||||
|
@property (assign) MPElementType type;
|
||||||
|
@property (readonly) NSString *typeName;
|
||||||
|
@property (readonly) NSString *typeShortName;
|
||||||
|
@property (readonly) NSString *typeClassName;
|
||||||
|
@property (readonly) Class typeClass;
|
||||||
|
@property (assign) NSUInteger uses;
|
||||||
|
@property (assign) NSUInteger version;
|
||||||
|
@property (assign) BOOL requiresExplicitMigration;
|
||||||
|
@property (readonly) id<MPAlgorithm> algorithm;
|
||||||
|
|
||||||
|
- (NSUInteger)use;
|
||||||
|
- (NSString *)exportContent;
|
||||||
|
- (void)importProtectedContent:(NSString *)protectedContent;
|
||||||
|
- (void)importClearTextContent:(NSString *)clearContent usingKey:(MPKey *)key;
|
||||||
|
- (BOOL)migrateExplicitly:(BOOL)explicit;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface MPElementGeneratedEntity (MP)
|
||||||
|
|
||||||
|
@property (assign) NSUInteger counter;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface MPUserEntity (MP)
|
||||||
|
|
||||||
|
@property (assign) NSUInteger avatar;
|
||||||
|
@property (assign) BOOL saveKey;
|
||||||
|
@property (assign) MPElementType defaultType;
|
||||||
|
@property (assign) BOOL requiresExplicitMigration;
|
||||||
|
@property (readonly) NSString *userID;
|
||||||
|
|
||||||
|
+ (NSString *)idFor:(NSString *)userName;
|
||||||
|
|
||||||
|
@end
|
||||||
299
MasterPassword/MPEntities.m
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
//
|
||||||
|
// MPElementEntities.m
|
||||||
|
// MasterPassword-iOS
|
||||||
|
//
|
||||||
|
// Created by Maarten Billemont on 31/05/12.
|
||||||
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "MPEntities.h"
|
||||||
|
#import "MPAppDelegate.h"
|
||||||
|
|
||||||
|
@implementation MPElementEntity (MP)
|
||||||
|
|
||||||
|
- (MPElementType)type {
|
||||||
|
|
||||||
|
return (MPElementType)[self.type_ unsignedIntegerValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setType:(MPElementType)aType {
|
||||||
|
|
||||||
|
self.type_ = PearlUnsignedInteger(aType);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)typeName {
|
||||||
|
|
||||||
|
return [self.algorithm nameOfType:self.type];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)typeShortName {
|
||||||
|
|
||||||
|
return [self.algorithm shortNameOfType:self.type];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)typeClassName {
|
||||||
|
|
||||||
|
return [self.algorithm classNameOfType:self.type];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (Class)typeClass {
|
||||||
|
|
||||||
|
return [self.algorithm classOfType:self.type];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSUInteger)uses {
|
||||||
|
|
||||||
|
return [self.uses_ unsignedIntegerValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setUses:(NSUInteger)anUses {
|
||||||
|
|
||||||
|
self.uses_ = PearlUnsignedInteger(anUses);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSUInteger)version {
|
||||||
|
|
||||||
|
return [self.version_ unsignedIntegerValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setVersion:(NSUInteger)version {
|
||||||
|
|
||||||
|
self.version_ = PearlUnsignedInteger(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)requiresExplicitMigration {
|
||||||
|
|
||||||
|
return [self.requiresExplicitMigration_ boolValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setRequiresExplicitMigration:(BOOL)requiresExplicitMigration {
|
||||||
|
|
||||||
|
self.requiresExplicitMigration_ = PearlBool(requiresExplicitMigration);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id<MPAlgorithm>)algorithm {
|
||||||
|
|
||||||
|
return MPAlgorithmForVersion(self.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSUInteger)use {
|
||||||
|
|
||||||
|
self.lastUsed = [NSDate date];
|
||||||
|
return ++self.uses;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id)content {
|
||||||
|
|
||||||
|
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Content implementation missing." userInfo:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)exportContent {
|
||||||
|
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)importProtectedContent:(NSString *)content {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)importClearTextContent:(NSString *)clearContent usingKey:(MPKey *)key {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)description {
|
||||||
|
|
||||||
|
return PearlString(@"%@:%@", [self class], [self name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)debugDescription {
|
||||||
|
|
||||||
|
return PearlString(@"{%@: name=%@, user=%@, type=%d, uses=%d, lastUsed=%@, version=%d, userName=%@, requiresExplicitMigration=%d}",
|
||||||
|
NSStringFromClass([self class]), self.name, self.user.name, self.type, self.uses, self.lastUsed, self.version,
|
||||||
|
self.userName, self.requiresExplicitMigration);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)migrateExplicitly:(BOOL)explicit {
|
||||||
|
|
||||||
|
while (self.version < MPAlgorithmDefaultVersion)
|
||||||
|
if ([MPAlgorithmForVersion(self.version + 1) migrateElement:self explicit:explicit])
|
||||||
|
inf(@"%@ migration to version: %d succeeded for element: %@", explicit? @"Explicit": @"Automatic", self.version + 1, self);
|
||||||
|
else {
|
||||||
|
wrn(@"%@ migration to version: %d failed for element: %@", explicit? @"Explicit": @"Automatic", self.version + 1, self);
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation MPElementGeneratedEntity (MP)
|
||||||
|
|
||||||
|
- (NSUInteger)counter {
|
||||||
|
|
||||||
|
return [self.counter_ unsignedIntegerValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setCounter:(NSUInteger)aCounter {
|
||||||
|
|
||||||
|
self.counter_ = PearlUnsignedInteger(aCounter);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id)content {
|
||||||
|
|
||||||
|
MPKey *key = [MPAppDelegate get].key;
|
||||||
|
if (!key)
|
||||||
|
return nil;
|
||||||
|
|
||||||
|
if (!(self.type & MPElementTypeClassGenerated)) {
|
||||||
|
err(@"Corrupt element: %@, type: %d is not in MPElementTypeClassGenerated", self.name, self.type);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (![self.name length])
|
||||||
|
return nil;
|
||||||
|
|
||||||
|
return [self.algorithm generateContentForElement:self usingKey:key];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation MPElementStoredEntity (MP)
|
||||||
|
|
||||||
|
+ (NSDictionary *)queryForDevicePrivateElementNamed:(NSString *)name {
|
||||||
|
|
||||||
|
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
||||||
|
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
|
@"DevicePrivate", (__bridge id)kSecAttrService,
|
||||||
|
name, (__bridge id)kSecAttrAccount,
|
||||||
|
nil]
|
||||||
|
matches:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id)content {
|
||||||
|
|
||||||
|
MPKey *key = [MPAppDelegate get].key;
|
||||||
|
if (!key)
|
||||||
|
return nil;
|
||||||
|
|
||||||
|
return [self contentUsingKey:key];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setContent:(id)content {
|
||||||
|
|
||||||
|
MPKey *key = [MPAppDelegate get].key;
|
||||||
|
if (!key)
|
||||||
|
return;
|
||||||
|
|
||||||
|
[self setContent:content usingKey:key];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id)contentUsingKey:(MPKey *)key {
|
||||||
|
|
||||||
|
assert(self.type & MPElementTypeClassStored);
|
||||||
|
assert([key.keyID isEqualToData:self.user.keyID]);
|
||||||
|
|
||||||
|
NSData *encryptedContent;
|
||||||
|
if (self.type & MPElementFeatureDevicePrivate)
|
||||||
|
encryptedContent = [PearlKeyChain dataOfItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]];
|
||||||
|
else
|
||||||
|
encryptedContent = self.contentObject;
|
||||||
|
|
||||||
|
NSData *decryptedContent = nil;
|
||||||
|
if ([encryptedContent length])
|
||||||
|
decryptedContent = [encryptedContent decryptWithSymmetricKey:[key subKeyOfLength:PearlCryptKeySize].keyData padding:YES];
|
||||||
|
|
||||||
|
return [[NSString alloc] initWithBytes:decryptedContent.bytes length:decryptedContent.length encoding:NSUTF8StringEncoding];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setContent:(id)content usingKey:(MPKey *)key {
|
||||||
|
|
||||||
|
assert(self.type & MPElementTypeClassStored);
|
||||||
|
assert([key.keyID isEqualToData:self.user.keyID]);
|
||||||
|
|
||||||
|
NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[key subKeyOfLength:PearlCryptKeySize].keyData padding:YES];
|
||||||
|
|
||||||
|
if (self.type & MPElementFeatureDevicePrivate) {
|
||||||
|
[PearlKeyChain addOrUpdateItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]
|
||||||
|
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
|
encryptedContent, (__bridge id)kSecValueData,
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
|
(__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
||||||
|
(__bridge id)kSecAttrAccessible,
|
||||||
|
#endif
|
||||||
|
nil]];
|
||||||
|
self.contentObject = nil;
|
||||||
|
} else
|
||||||
|
self.contentObject = encryptedContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)exportContent {
|
||||||
|
|
||||||
|
return [self.contentObject encodeBase64];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)importProtectedContent:(NSString *)protectedContent {
|
||||||
|
|
||||||
|
self.contentObject = [protectedContent decodeBase64];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)importClearTextContent:(NSString *)clearContent usingKey:(MPKey *)key {
|
||||||
|
|
||||||
|
[self setContent:clearContent usingKey:key];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation MPUserEntity (MP)
|
||||||
|
|
||||||
|
- (NSUInteger)avatar {
|
||||||
|
|
||||||
|
return [self.avatar_ unsignedIntegerValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setAvatar:(NSUInteger)anAvatar {
|
||||||
|
|
||||||
|
self.avatar_ = PearlUnsignedInteger(anAvatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)saveKey {
|
||||||
|
|
||||||
|
return [self.saveKey_ boolValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setSaveKey:(BOOL)aSaveKey {
|
||||||
|
|
||||||
|
self.saveKey_ = [NSNumber numberWithBool:aSaveKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MPElementType)defaultType {
|
||||||
|
|
||||||
|
return (MPElementType)[self.defaultType_ unsignedIntegerValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setDefaultType:(MPElementType)aDefaultType {
|
||||||
|
|
||||||
|
self.defaultType_ = PearlUnsignedInteger(aDefaultType);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)requiresExplicitMigration {
|
||||||
|
|
||||||
|
return [self.requiresExplicitMigration_ boolValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setRequiresExplicitMigration:(BOOL)requiresExplicitMigration {
|
||||||
|
|
||||||
|
self.requiresExplicitMigration_ = PearlBool(requiresExplicitMigration);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)userID {
|
||||||
|
|
||||||
|
return [MPUserEntity idFor:self.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (NSString *)idFor:(NSString *)userName {
|
||||||
|
|
||||||
|
return [[userName hashWith:PearlHashSHA1] encodeHex];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
33
MasterPassword/MPKey.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
|
||||||
|
*
|
||||||
|
* See the enclosed file LICENSE for license information (LGPLv3). If you did
|
||||||
|
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*
|
||||||
|
* @author Maarten Billemont <lhunath@lyndir.com>
|
||||||
|
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
//
|
||||||
|
// MPKey
|
||||||
|
//
|
||||||
|
// Created by Maarten Billemont on 16/07/12.
|
||||||
|
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
@protocol MPAlgorithm;
|
||||||
|
|
||||||
|
|
||||||
|
@interface MPKey : NSObject
|
||||||
|
|
||||||
|
@property (nonatomic, readonly, strong) id<MPAlgorithm> algorithm;
|
||||||
|
@property (nonatomic, readonly, strong) NSData *keyData;
|
||||||
|
@property (nonatomic, readonly, strong) NSData *keyID;
|
||||||
|
|
||||||
|
- (id)initWithKeyData:(NSData *)keyData algorithm:(id<MPAlgorithm>)algorithm;
|
||||||
|
- (MPKey *)subKeyOfLength:(NSUInteger)subKeyLength;
|
||||||
|
- (BOOL)isEqualToKey:(MPKey *)key;
|
||||||
|
|
||||||
|
@end
|
||||||
66
MasterPassword/MPKey.m
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com)
|
||||||
|
*
|
||||||
|
* See the enclosed file LICENSE for license information (LGPLv3). If you did
|
||||||
|
* not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*
|
||||||
|
* @author Maarten Billemont <lhunath@lyndir.com>
|
||||||
|
* @license http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
//
|
||||||
|
// MPKey
|
||||||
|
//
|
||||||
|
// Created by Maarten Billemont on 16/07/12.
|
||||||
|
// Copyright 2012 lhunath (Maarten Billemont). All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "MPKey.h"
|
||||||
|
#import "MPAlgorithm.h"
|
||||||
|
|
||||||
|
|
||||||
|
@interface MPKey ()
|
||||||
|
|
||||||
|
@property (nonatomic, readwrite, strong) id<MPAlgorithm> algorithm;
|
||||||
|
@property (nonatomic, readwrite, strong) NSData *keyData;
|
||||||
|
@property (nonatomic, readwrite, strong) NSData *keyID;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation MPKey
|
||||||
|
@synthesize algorithm = _algorithm, keyData = _keyData, keyID = _keyID;
|
||||||
|
|
||||||
|
- (id)initWithKeyData:(NSData *)keyData algorithm:(id<MPAlgorithm>)algorithm {
|
||||||
|
|
||||||
|
if (!(self = [super init]))
|
||||||
|
return nil;
|
||||||
|
|
||||||
|
self.keyData = keyData;
|
||||||
|
self.algorithm = algorithm;
|
||||||
|
self.keyID = [self.algorithm keyIDForKeyData:keyData];
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MPKey *)subKeyOfLength:(NSUInteger)subKeyLength {
|
||||||
|
|
||||||
|
NSData *subKeyData = [self.keyData subdataWithRange:NSMakeRange(0, MIN(subKeyLength, self.keyData.length))];
|
||||||
|
|
||||||
|
return [self.algorithm keyFromKeyData:subKeyData];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isEqualToKey:(MPKey *)key {
|
||||||
|
|
||||||
|
return [self.keyID isEqualToData:key.keyID];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isEqual:(id)object {
|
||||||
|
|
||||||
|
if (![object isKindOfClass:[MPKey class]])
|
||||||
|
return NO;
|
||||||
|
|
||||||
|
return [self isEqualToKey:object];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -6,9 +6,7 @@
|
|||||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import "MPKey.h"
|
||||||
|
|
||||||
#define MPPersistentStoreDidChangeNotification @"MPPersistentStoreDidChange"
|
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
MPElementContentTypePassword,
|
MPElementContentTypePassword,
|
||||||
@@ -18,68 +16,64 @@ typedef enum {
|
|||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
/** Generate the password. */
|
/** Generate the password. */
|
||||||
MPElementTypeClassGenerated = 1 << 4,
|
MPElementTypeClassGenerated = 1 << 4,
|
||||||
/** Store the password. */
|
/** Store the password. */
|
||||||
MPElementTypeClassStored = 1 << 5,
|
MPElementTypeClassStored = 1 << 5,
|
||||||
} MPElementTypeClass;
|
} MPElementTypeClass;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
/** Export the key-protected content data. */
|
/** Export the key-protected content data. */
|
||||||
MPElementFeatureExportContent = 1 << 10,
|
MPElementFeatureExportContent = 1 << 10,
|
||||||
/** Never export content. */
|
/** Never export content. */
|
||||||
MPElementFeatureDevicePrivate = 1 << 11,
|
MPElementFeatureDevicePrivate = 1 << 11,
|
||||||
} MPElementFeature;
|
} MPElementFeature;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
MPElementTypeGeneratedLong = 0x0 | MPElementTypeClassGenerated | 0x0,
|
MPElementTypeGeneratedMaximum = 0x0 | MPElementTypeClassGenerated | 0x0,
|
||||||
MPElementTypeGeneratedMedium = 0x1 | MPElementTypeClassGenerated | 0x0,
|
MPElementTypeGeneratedLong = 0x1 | MPElementTypeClassGenerated | 0x0,
|
||||||
MPElementTypeGeneratedShort = 0x2 | MPElementTypeClassGenerated | 0x0,
|
MPElementTypeGeneratedMedium = 0x2 | MPElementTypeClassGenerated | 0x0,
|
||||||
MPElementTypeGeneratedBasic = 0x3 | MPElementTypeClassGenerated | 0x0,
|
MPElementTypeGeneratedShort = 0x3 | MPElementTypeClassGenerated | 0x0,
|
||||||
MPElementTypeGeneratedPIN = 0x4 | MPElementTypeClassGenerated | 0x0,
|
MPElementTypeGeneratedBasic = 0x4 | MPElementTypeClassGenerated | 0x0,
|
||||||
|
MPElementTypeGeneratedPIN = 0x5 | MPElementTypeClassGenerated | 0x0,
|
||||||
|
|
||||||
MPElementTypeStoredPersonal = 0x0 | MPElementTypeClassStored | MPElementFeatureExportContent,
|
MPElementTypeStoredPersonal = 0x0 | MPElementTypeClassStored | MPElementFeatureExportContent,
|
||||||
MPElementTypeStoredDevicePrivate = 0x1 | MPElementTypeClassStored | MPElementFeatureDevicePrivate,
|
MPElementTypeStoredDevicePrivate = 0x1 | MPElementTypeClassStored | MPElementFeatureDevicePrivate,
|
||||||
} MPElementType;
|
} MPElementType;
|
||||||
|
|
||||||
#define MPTestFlightCheckpointAction @"MPTestFlightCheckpointAction"
|
#define MPCheckpointAction @"MPCheckpointAction"
|
||||||
#define MPTestFlightCheckpointHelpChapter @"MPTestFlightCheckpointHelpChapter_%@"
|
#define MPCheckpointHelpChapter @"MPCheckpointHelpChapter"
|
||||||
#define MPTestFlightCheckpointCopyToPasteboard @"MPTestFlightCheckpointCopyToPasteboard"
|
#define MPCheckpointCopyToPasteboard @"MPCheckpointCopyToPasteboard"
|
||||||
#define MPTestFlightCheckpointResetPasswordCounter @"MPTestFlightCheckpointResetPasswordCounter"
|
#define MPCheckpointCopyUserNameToPasteboard @"MPCheckpointCopyUserNameToPasteboard"
|
||||||
#define MPTestFlightCheckpointIncrementPasswordCounter @"MPTestFlightCheckpointIncrementPasswordCounter"
|
#define MPCheckpointResetPasswordCounter @"MPCheckpointResetPasswordCounter"
|
||||||
#define MPTestFlightCheckpointEditPassword @"MPTestFlightCheckpointEditPassword"
|
#define MPCheckpointIncrementPasswordCounter @"MPCheckpointIncrementPasswordCounter"
|
||||||
#define MPTestFlightCheckpointCloseAlert @"MPTestFlightCheckpointCloseAlert"
|
#define MPCheckpointEditPassword @"MPCheckpointEditPassword"
|
||||||
#define MPTestFlightCheckpointSelectType @"MPTestFlightCheckpointSelectType_%@"
|
#define MPCheckpointEditUserName @"MPCheckpointEditUserName"
|
||||||
#define MPTestFlightCheckpointSelectElement @"MPTestFlightCheckpointSelectElement"
|
#define MPCheckpointCloseAlert @"MPCheckpointCloseAlert"
|
||||||
#define MPTestFlightCheckpointDeleteElement @"MPTestFlightCheckpointDeleteElement"
|
#define MPCheckpointCloseOutdatedAlert @"MPCheckpointCloseOutdatedAlert"
|
||||||
#define MPTestFlightCheckpointCancelSearch @"MPTestFlightCheckpointCancelSearch"
|
#define MPCheckpointUseType @"MPCheckpointUseType"
|
||||||
#define MPTestFlightCheckpointExternalLink @"MPTestFlightCheckpointExternalLink"
|
#define MPCheckpointDeleteElement @"MPCheckpointDeleteElement"
|
||||||
#define MPTestFlightCheckpointLaunched @"MPTestFlightCheckpointLaunched"
|
#define MPCheckpointCancelSearch @"MPCheckpointCancelSearch"
|
||||||
#define MPTestFlightCheckpointActivated @"MPTestFlightCheckpointActivated"
|
#define MPCheckpointExternalLink @"MPCheckpointExternalLink"
|
||||||
#define MPTestFlightCheckpointDeactivated @"MPTestFlightCheckpointDeactivated"
|
#define MPCheckpointLaunched @"MPCheckpointLaunched"
|
||||||
#define MPTestFlightCheckpointTerminated @"MPTestFlightCheckpointTerminated"
|
#define MPCheckpointActivated @"MPCheckpointActivated"
|
||||||
#define MPTestFlightCheckpointShowGuide @"MPTestFlightCheckpointShowGuide"
|
#define MPCheckpointDeactivated @"MPCheckpointDeactivated"
|
||||||
#define MPTestFlightCheckpointMPForgotten @"MPTestFlightCheckpointMPForgotten"
|
#define MPCheckpointTerminated @"MPCheckpointTerminated"
|
||||||
#define MPTestFlightCheckpointMPChanged @"MPTestFlightCheckpointMPChanged"
|
#define MPCheckpointShowGuide @"MPCheckpointShowGuide"
|
||||||
#define MPTestFlightCheckpointMPUnstored @"MPTestFlightCheckpointMPUnstored"
|
#define MPCheckpointForgetSavedKey @"MPCheckpointForgetSavedKey"
|
||||||
#define MPTestFlightCheckpointMPMismatch @"MPTestFlightCheckpointMPMismatch"
|
#define MPCheckpointChangeMP @"MPCheckpointChangeMP"
|
||||||
#define MPTestFlightCheckpointMPEntered @"MPTestFlightCheckpointMPEntered"
|
#define MPCheckpointLocalStoreIncompatible @"MPCheckpointLocalStoreIncompatible"
|
||||||
#define MPTestFlightCheckpointLocalStoreIncompatible @"MPTestFlightCheckpointLocalStoreIncompatible"
|
#define MPCheckpointCloudStoreIncompatible @"MPCheckpointCloudStoreIncompatible"
|
||||||
#define MPTestFlightCheckpointCloudStoreIncompatible @"MPTestFlightCheckpointCloudStoreIncompatible"
|
#define MPCheckpointSignInFailed @"MPCheckpointSignInFailed"
|
||||||
#define MPTestFlightCheckpointSetKey @"MPTestFlightCheckpointSetKey"
|
#define MPCheckpointSignedIn @"MPCheckpointSignedIn"
|
||||||
#define MPTestFlightCheckpointCloudEnabled @"MPTestFlightCheckpointCloudEnabled"
|
#define MPCheckpointConfig @"MPCheckpointConfig"
|
||||||
#define MPTestFlightCheckpointCloudDisabled @"MPTestFlightCheckpointCloudDisabled"
|
#define MPCheckpointCloud @"MPCheckpointCloud"
|
||||||
#define MPTestFlightCheckpointSitesImported @"MPTestFlightCheckpointSitesImported"
|
#define MPCheckpointCloudEnabled @"MPCheckpointCloudEnabled"
|
||||||
#define MPTestFlightCheckpointSitesExported @"MPTestFlightCheckpointSitesExported"
|
#define MPCheckpointCloudDisabled @"MPCheckpointCloudDisabled"
|
||||||
|
#define MPCheckpointSitesImported @"MPCheckpointSitesImported"
|
||||||
|
#define MPCheckpointSitesExported @"MPCheckpointSitesExported"
|
||||||
|
#define MPCheckpointExplicitMigration @"MPCheckpointExplicitMigration"
|
||||||
|
|
||||||
#define MPNotificationStoreUpdated @"MPNotificationStoreUpdated"
|
#define MPNotificationSignedIn @"MPNotificationKeySet"
|
||||||
#define MPNotificationKeySet @"MPNotificationKeySet"
|
#define MPNotificationSignedOut @"MPNotificationKeyUnset"
|
||||||
#define MPNotificationKeyUnset @"MPNotificationKeyUnset"
|
#define MPNotificationKeyForgotten @"MPNotificationKeyForgotten"
|
||||||
#define MPNotificationKeyForgotten @"MPNotificationKeyForgotten"
|
#define MPNotificationElementUpdated @"MPNotificationElementUpdated"
|
||||||
|
|
||||||
NSData *keyForPassword(NSString *password);
|
|
||||||
NSData *keyIDForPassword(NSString *password);
|
|
||||||
NSData *keyIDForKey(NSData *key);
|
|
||||||
NSString *NSStringFromMPElementType(MPElementType type);
|
|
||||||
NSString *ClassNameFromMPElementType(MPElementType type);
|
|
||||||
Class ClassFromMPElementType(MPElementType type);
|
|
||||||
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, int32_t counter);
|
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
//
|
|
||||||
// MPTypes.m
|
|
||||||
// MasterPassword
|
|
||||||
//
|
|
||||||
// Created by Maarten Billemont on 02/01/12.
|
|
||||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#import "MPTypes.h"
|
|
||||||
#import "MPElementGeneratedEntity.h"
|
|
||||||
#import "MPElementStoredEntity.h"
|
|
||||||
|
|
||||||
|
|
||||||
#define MP_salt nil
|
|
||||||
#define MP_N 16384
|
|
||||||
#define MP_r 8
|
|
||||||
#define MP_p 1
|
|
||||||
#define MP_dkLen 64
|
|
||||||
#define MP_hash PearlDigestSHA256
|
|
||||||
|
|
||||||
NSData *keyForPassword(NSString *password) {
|
|
||||||
|
|
||||||
NSData *key = [PearlSCrypt deriveKeyWithLength:MP_dkLen fromPassword:[password dataUsingEncoding:NSUTF8StringEncoding]
|
|
||||||
usingSalt:MP_salt N:MP_N r:MP_r p:MP_p];
|
|
||||||
|
|
||||||
trc(@"Password: %@ derives to key ID: %@", password, [keyIDForKey(key) encodeHex]);
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
NSData *keyIDForPassword(NSString *password) {
|
|
||||||
|
|
||||||
return keyIDForKey(keyForPassword(password));
|
|
||||||
}
|
|
||||||
NSData *keyIDForKey(NSData *key) {
|
|
||||||
|
|
||||||
return [key hashWith:MP_hash];
|
|
||||||
}
|
|
||||||
NSString *NSStringFromMPElementType(MPElementType type) {
|
|
||||||
|
|
||||||
if (!type)
|
|
||||||
return nil;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case MPElementTypeGeneratedLong:
|
|
||||||
return @"Long Password";
|
|
||||||
|
|
||||||
case MPElementTypeGeneratedMedium:
|
|
||||||
return @"Medium Password";
|
|
||||||
|
|
||||||
case MPElementTypeGeneratedShort:
|
|
||||||
return @"Short Password";
|
|
||||||
|
|
||||||
case MPElementTypeGeneratedBasic:
|
|
||||||
return @"Basic Password";
|
|
||||||
|
|
||||||
case MPElementTypeGeneratedPIN:
|
|
||||||
return @"PIN";
|
|
||||||
|
|
||||||
case MPElementTypeStoredPersonal:
|
|
||||||
return @"Personal Password";
|
|
||||||
|
|
||||||
case MPElementTypeStoredDevicePrivate:
|
|
||||||
return @"Device Private Password";
|
|
||||||
|
|
||||||
default:
|
|
||||||
[NSException raise:NSInternalInconsistencyException format:@"Type not supported: %d", type];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Class ClassFromMPElementType(MPElementType type) {
|
|
||||||
|
|
||||||
if (!type)
|
|
||||||
return nil;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case MPElementTypeGeneratedLong:
|
|
||||||
return [MPElementGeneratedEntity class];
|
|
||||||
|
|
||||||
case MPElementTypeGeneratedMedium:
|
|
||||||
return [MPElementGeneratedEntity class];
|
|
||||||
|
|
||||||
case MPElementTypeGeneratedShort:
|
|
||||||
return [MPElementGeneratedEntity class];
|
|
||||||
|
|
||||||
case MPElementTypeGeneratedBasic:
|
|
||||||
return [MPElementGeneratedEntity class];
|
|
||||||
|
|
||||||
case MPElementTypeGeneratedPIN:
|
|
||||||
return [MPElementGeneratedEntity class];
|
|
||||||
|
|
||||||
case MPElementTypeStoredPersonal:
|
|
||||||
return [MPElementStoredEntity class];
|
|
||||||
|
|
||||||
case MPElementTypeStoredDevicePrivate:
|
|
||||||
return [MPElementStoredEntity class];
|
|
||||||
|
|
||||||
default:
|
|
||||||
[NSException raise:NSInternalInconsistencyException format:@"Type not supported: %d", type];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *ClassNameFromMPElementType(MPElementType type) {
|
|
||||||
|
|
||||||
return NSStringFromClass(ClassFromMPElementType(type));
|
|
||||||
}
|
|
||||||
|
|
||||||
static NSDictionary *MPTypes_ciphers = nil;
|
|
||||||
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, int32_t counter) {
|
|
||||||
|
|
||||||
if (!(type & MPElementTypeClassGenerated)) {
|
|
||||||
err(@"Incorrect type (is not MPElementTypeClassGenerated): %d, for: %@", type, name);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
if (!name) {
|
|
||||||
err(@"Missing name.");
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
if (!key) {
|
|
||||||
err(@"Key not set.");
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
uint32_t salt = (unsigned)counter;
|
|
||||||
if (!counter)
|
|
||||||
// Counter unset, go into OTP mode.
|
|
||||||
// Get the UNIX timestamp of the start of the interval of 5 minutes that the current time is in.
|
|
||||||
salt = ((uint32_t)([[NSDate date] timeIntervalSince1970] / 300)) * 300;
|
|
||||||
|
|
||||||
if (MPTypes_ciphers == nil)
|
|
||||||
MPTypes_ciphers = [NSDictionary dictionaryWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"ciphers"
|
|
||||||
withExtension:@"plist"]];
|
|
||||||
|
|
||||||
// Determine the seed whose bytes will be used for calculating a password: sha1(name . '\0' . key . '\0' . salt)
|
|
||||||
uint32_t nsalt = htonl(salt);
|
|
||||||
trc(@"seed from: sha1(%@, %@, %u)", name, key, nsalt);
|
|
||||||
NSData *seed = [[NSData dataByConcatenatingWithDelimitor:'\0' datas:
|
|
||||||
[name dataUsingEncoding:NSUTF8StringEncoding],
|
|
||||||
key,
|
|
||||||
[NSData dataWithBytes:&nsalt length:sizeof(nsalt)],
|
|
||||||
nil] hashWith:PearlDigestSHA1];
|
|
||||||
trc(@"seed is: %@", seed);
|
|
||||||
const char *seedBytes = seed.bytes;
|
|
||||||
|
|
||||||
// Determine the cipher from the first seed byte.
|
|
||||||
assert([seed length]);
|
|
||||||
NSArray *typeCiphers = [[MPTypes_ciphers valueForKey:ClassNameFromMPElementType(type)]
|
|
||||||
valueForKey:NSStringFromMPElementType(type)];
|
|
||||||
NSString *cipher = [typeCiphers objectAtIndex:htons(seedBytes[0]) % [typeCiphers count]];
|
|
||||||
trc(@"type %d, ciphers: %@, selected: %@", type, typeCiphers, cipher);
|
|
||||||
|
|
||||||
// Encode the content, character by character, using subsequent seed bytes and the cipher.
|
|
||||||
assert([seed length] >= [cipher length] + 1);
|
|
||||||
NSMutableString *content = [NSMutableString stringWithCapacity:[cipher length]];
|
|
||||||
for (NSUInteger c = 0; c < [cipher length]; ++c) {
|
|
||||||
uint16_t keyByte = htons(seedBytes[c + 1]);
|
|
||||||
NSString *cipherClass = [cipher substringWithRange:NSMakeRange(c, 1)];
|
|
||||||
NSString *cipherClassCharacters = [[MPTypes_ciphers valueForKey:@"MPCharacterClasses"] valueForKey:cipherClass];
|
|
||||||
NSString *character = [cipherClassCharacters substringWithRange:NSMakeRange(keyByte % [cipherClassCharacters length], 1)];
|
|
||||||
|
|
||||||
trc(@"class %@ has characters: %@, selected: %@", cipherClass, cipherClassCharacters, character);
|
|
||||||
[content appendString:character];
|
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
33
MasterPassword/MPUserEntity.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// MPUserEntity.h
|
||||||
|
// MasterPassword-iOS
|
||||||
|
//
|
||||||
|
// Created by Maarten Billemont on 17/07/12.
|
||||||
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <CoreData/CoreData.h>
|
||||||
|
|
||||||
|
@class MPElementEntity;
|
||||||
|
|
||||||
|
@interface MPUserEntity : NSManagedObject
|
||||||
|
|
||||||
|
@property (nonatomic, retain) NSNumber * avatar_;
|
||||||
|
@property (nonatomic, retain) NSNumber * defaultType_;
|
||||||
|
@property (nonatomic, retain) NSData * keyID;
|
||||||
|
@property (nonatomic, retain) NSDate * lastUsed;
|
||||||
|
@property (nonatomic, retain) NSString * name;
|
||||||
|
@property (nonatomic, retain) NSNumber * saveKey_;
|
||||||
|
@property (nonatomic, retain) NSNumber * requiresExplicitMigration_;
|
||||||
|
@property (nonatomic, retain) NSSet *elements;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface MPUserEntity (CoreDataGeneratedAccessors)
|
||||||
|
|
||||||
|
- (void)addElementsObject:(MPElementEntity *)value;
|
||||||
|
- (void)removeElementsObject:(MPElementEntity *)value;
|
||||||
|
- (void)addElements:(NSSet *)values;
|
||||||
|
- (void)removeElements:(NSSet *)values;
|
||||||
|
|
||||||
|
@end
|
||||||
24
MasterPassword/MPUserEntity.m
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// MPUserEntity.m
|
||||||
|
// MasterPassword-iOS
|
||||||
|
//
|
||||||
|
// Created by Maarten Billemont on 17/07/12.
|
||||||
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "MPUserEntity.h"
|
||||||
|
#import "MPElementEntity.h"
|
||||||
|
|
||||||
|
|
||||||
|
@implementation MPUserEntity
|
||||||
|
|
||||||
|
@dynamic avatar_;
|
||||||
|
@dynamic defaultType_;
|
||||||
|
@dynamic keyID;
|
||||||
|
@dynamic lastUsed;
|
||||||
|
@dynamic name;
|
||||||
|
@dynamic saveKey_;
|
||||||
|
@dynamic requiresExplicitMigration_;
|
||||||
|
@dynamic elements;
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -10,16 +10,16 @@
|
|||||||
#import "MPAppDelegate_Shared.h"
|
#import "MPAppDelegate_Shared.h"
|
||||||
#import "MPPasswordWindowController.h"
|
#import "MPPasswordWindowController.h"
|
||||||
|
|
||||||
@interface MPAppDelegate : MPAppDelegate_Shared <NSApplicationDelegate>
|
@interface MPAppDelegate : MPAppDelegate_Shared<NSApplicationDelegate>
|
||||||
|
|
||||||
@property (strong) NSStatusItem *statusItem;
|
@property (strong) NSStatusItem *statusItem;
|
||||||
@property (strong) MPPasswordWindowController *passwordWindow;
|
@property (strong) MPPasswordWindowController *passwordWindow;
|
||||||
@property (weak) IBOutlet NSMenuItem *lockItem;
|
@property (weak) IBOutlet NSMenuItem *lockItem;
|
||||||
@property (weak) IBOutlet NSMenuItem *showItem;
|
@property (weak) IBOutlet NSMenuItem *showItem;
|
||||||
@property (strong) IBOutlet NSMenu *statusMenu;
|
@property (strong) IBOutlet NSMenu *statusMenu;
|
||||||
@property (weak) IBOutlet NSMenuItem *useICloudItem;
|
@property (weak) IBOutlet NSMenuItem *useICloudItem;
|
||||||
@property (weak) IBOutlet NSMenuItem *rememberPasswordItem;
|
@property (weak) IBOutlet NSMenuItem *rememberPasswordItem;
|
||||||
@property (weak) IBOutlet NSMenuItem *savePasswordItem;
|
@property (weak) IBOutlet NSMenuItem *savePasswordItem;
|
||||||
|
|
||||||
+ (MPAppDelegate *)get;
|
+ (MPAppDelegate *)get;
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,10 @@
|
|||||||
@synthesize key;
|
@synthesize key;
|
||||||
@synthesize keyID;
|
@synthesize keyID;
|
||||||
|
|
||||||
#pragma GCC diagnostic ignored "-Wfour-char-constants"
|
#pragma clang diagnostic push
|
||||||
static EventHotKeyID MPShowHotKey = { .signature = 'show', .id = 1 };
|
#pragma clang diagnostic ignored "-Wfour-char-constants"
|
||||||
|
static EventHotKeyID MPShowHotKey = {.signature = 'show', .id = 1};
|
||||||
|
#pragma clang diagnostic pop
|
||||||
|
|
||||||
+ (void)initialize {
|
+ (void)initialize {
|
||||||
|
|
||||||
@@ -44,12 +46,12 @@ static EventHotKeyID MPShowHotKey = { .signature = 'show', .id = 1 };
|
|||||||
return (MPAppDelegate *)[super get];
|
return (MPAppDelegate *)[super get];
|
||||||
}
|
}
|
||||||
|
|
||||||
static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData){
|
static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData) {
|
||||||
|
|
||||||
// Extract the hotkey ID.
|
// Extract the hotkey ID.
|
||||||
EventHotKeyID hotKeyID;
|
EventHotKeyID hotKeyID;
|
||||||
GetEventParameter(theEvent,kEventParamDirectObject,typeEventHotKeyID,
|
GetEventParameter(theEvent, kEventParamDirectObject, typeEventHotKeyID,
|
||||||
NULL,sizeof(hotKeyID),NULL,&hotKeyID);
|
NULL, sizeof(hotKeyID), NULL, &hotKeyID);
|
||||||
|
|
||||||
// Check which hotkey this was.
|
// Check which hotkey this was.
|
||||||
if (hotKeyID.signature == MPShowHotKey.signature && hotKeyID.id == MPShowHotKey.id) {
|
if (hotKeyID.signature == MPShowHotKey.signature && hotKeyID.id == MPShowHotKey.id) {
|
||||||
@@ -63,8 +65,8 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven
|
|||||||
- (void)showMenu {
|
- (void)showMenu {
|
||||||
|
|
||||||
self.rememberPasswordItem.state = [[MPConfig get].rememberKey boolValue]? NSOnState: NSOffState;
|
self.rememberPasswordItem.state = [[MPConfig get].rememberKey boolValue]? NSOnState: NSOffState;
|
||||||
self.savePasswordItem.state = [[MPConfig get].saveKey boolValue]? NSOnState: NSOffState;
|
self.savePasswordItem.state = [[MPConfig get].saveKey boolValue]? NSOnState: NSOffState;
|
||||||
self.showItem.enabled = ![self.passwordWindow.window isVisible];
|
self.showItem.enabled = ![self.passwordWindow.window isVisible];
|
||||||
|
|
||||||
[self.statusItem popUpStatusItemMenu:self.statusMenu];
|
[self.statusItem popUpStatusItemMenu:self.statusMenu];
|
||||||
}
|
}
|
||||||
@@ -84,7 +86,7 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven
|
|||||||
if (sender == rememberPasswordItem)
|
if (sender == rememberPasswordItem)
|
||||||
[MPConfig get].rememberKey = [NSNumber numberWithBool:![[MPConfig get].rememberKey boolValue]];
|
[MPConfig get].rememberKey = [NSNumber numberWithBool:![[MPConfig get].rememberKey boolValue]];
|
||||||
if (sender == savePasswordItem)
|
if (sender == savePasswordItem)
|
||||||
[MPConfig get].saveKey = [NSNumber numberWithBool:![[MPConfig get].saveKey boolValue]];
|
[MPConfig get].saveKey = [NSNumber numberWithBool:![[MPConfig get].saveKey boolValue]];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)oldValue {
|
- (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)oldValue {
|
||||||
@@ -92,7 +94,7 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven
|
|||||||
if (configKey == @selector(rememberKey))
|
if (configKey == @selector(rememberKey))
|
||||||
self.rememberPasswordItem.state = [[MPConfig get].rememberKey boolValue]? NSOnState: NSOffState;
|
self.rememberPasswordItem.state = [[MPConfig get].rememberKey boolValue]? NSOnState: NSOffState;
|
||||||
if (configKey == @selector(saveKey))
|
if (configKey == @selector(saveKey))
|
||||||
self.savePasswordItem.state = [[MPConfig get].saveKey boolValue]? NSOnState: NSOffState;
|
self.savePasswordItem.state = [[MPConfig get].saveKey boolValue]? NSOnState: NSOffState;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
|
||||||
@@ -125,22 +127,23 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven
|
|||||||
[[self storeManager] useiCloudStore:YES alertUser:YES];
|
[[self storeManager] useiCloudStore:YES alertUser:YES];
|
||||||
|
|
||||||
// Status item.
|
// Status item.
|
||||||
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
|
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
|
||||||
self.statusItem.title = @"•••";
|
self.statusItem.title = @"•••";
|
||||||
self.statusItem.highlightMode = YES;
|
self.statusItem.highlightMode = YES;
|
||||||
self.statusItem.target = self;
|
self.statusItem.target = self;
|
||||||
self.statusItem.action = @selector(showMenu);
|
self.statusItem.action = @selector(showMenu);
|
||||||
|
|
||||||
// Global hotkey.
|
// Global hotkey.
|
||||||
EventHotKeyRef hotKeyRef;
|
EventHotKeyRef hotKeyRef;
|
||||||
EventTypeSpec hotKeyEvents[1] = { { .eventClass = kEventClassKeyboard, .eventKind = kEventHotKeyPressed } };
|
EventTypeSpec hotKeyEvents[1] = {{.eventClass = kEventClassKeyboard, .eventKind = kEventHotKeyPressed}};
|
||||||
OSStatus status = InstallApplicationEventHandler(NewEventHandlerUPP(MPHotKeyHander), GetEventTypeCount(hotKeyEvents), hotKeyEvents,
|
OSStatus status = InstallApplicationEventHandler(NewEventHandlerUPP(MPHotKeyHander), GetEventTypeCount(hotKeyEvents),
|
||||||
(__bridge void *)self, NULL);
|
hotKeyEvents,
|
||||||
if(status != noErr)
|
(__bridge void *)self, NULL);
|
||||||
err(@"Error installing application event handler: %d", status);
|
if (status != noErr)
|
||||||
status = RegisterEventHotKey(35 /* p */, controlKey + cmdKey, MPShowHotKey, GetApplicationEventTarget(), 0, &hotKeyRef);
|
err(@"Error installing application event handler: %d", status);
|
||||||
if(status != noErr)
|
status = RegisterEventHotKey(35 /* p */, controlKey + cmdKey, MPShowHotKey, GetApplicationEventTarget(), 0, &hotKeyRef);
|
||||||
err(@"Error registering hotkey: %d", status);
|
if (status != noErr)
|
||||||
|
err(@"Error registering hotkey: %d", status);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)applicationWillBecomeActive:(NSNotification *)notification {
|
- (void)applicationWillBecomeActive:(NSNotification *)notification {
|
||||||
@@ -164,8 +167,7 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven
|
|||||||
self.key = nil;
|
self.key = nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
|
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
|
||||||
{
|
|
||||||
// Save changes in the application's managed object context before the application terminates.
|
// Save changes in the application's managed object context before the application terminates.
|
||||||
|
|
||||||
if (![self managedObjectContext]) {
|
if (![self managedObjectContext]) {
|
||||||
@@ -190,11 +192,11 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven
|
|||||||
return NSTerminateCancel;
|
return NSTerminateCancel;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString *question = NSLocalizedString(@"Could not save changes while quitting. Quit anyway?", @"Quit without saves error question message");
|
NSString *question = NSLocalizedString(@"Could not save changes while quitting. Quit anyway?", @"Quit without saves error question message");
|
||||||
NSString *info = NSLocalizedString(@"Quitting now will lose any changes you have made since the last successful save", @"Quit without saves error question info");
|
NSString *info = NSLocalizedString(@"Quitting now will lose any changes you have made since the last successful save", @"Quit without saves error question info");
|
||||||
NSString *quitButton = NSLocalizedString(@"Quit anyway", @"Quit anyway button title");
|
NSString *quitButton = NSLocalizedString(@"Quit anyway", @"Quit anyway button title");
|
||||||
NSString *cancelButton = NSLocalizedString(@"Cancel", @"Cancel button title");
|
NSString *cancelButton = NSLocalizedString(@"Cancel", @"Cancel button title");
|
||||||
NSAlert *alert = [[NSAlert alloc] init];
|
NSAlert *alert = [[NSAlert alloc] init];
|
||||||
[alert setMessageText:question];
|
[alert setMessageText:question];
|
||||||
[alert setInformativeText:info];
|
[alert setInformativeText:info];
|
||||||
[alert addButtonWithTitle:quitButton];
|
[alert addButtonWithTitle:quitButton];
|
||||||
@@ -214,7 +216,7 @@ static OSStatus MPHotKeyHander(EventHandlerCallRef nextHandler, EventRef theEven
|
|||||||
|
|
||||||
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToiCloud:(BOOL)iCloudEnabled {
|
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToiCloud:(BOOL)iCloudEnabled {
|
||||||
|
|
||||||
self.useICloudItem.state = iCloudEnabled? NSOnState: NSOffState;
|
self.useICloudItem.state = iCloudEnabled? NSOnState: NSOffState;
|
||||||
self.useICloudItem.enabled = !iCloudEnabled;
|
self.useICloudItem.enabled = !iCloudEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
@interface MPPasswordWindowController : NSWindowController <NSTextFieldDelegate> {
|
@interface MPPasswordWindowController : NSWindowController<NSTextFieldDelegate> {
|
||||||
|
|
||||||
NSString *_content;
|
NSString *_content;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,18 +59,18 @@
|
|||||||
- (void)unlock {
|
- (void)unlock {
|
||||||
|
|
||||||
if (![MPAppDelegate get].key)
|
if (![MPAppDelegate get].key)
|
||||||
// Try and load the key from the keychain.
|
// Try and load the key from the keychain.
|
||||||
[[MPAppDelegate get] loadStoredKey];
|
[[MPAppDelegate get] loadStoredKey];
|
||||||
|
|
||||||
if (![MPAppDelegate get].key)
|
if (![MPAppDelegate get].key)
|
||||||
// Ask the user to set the key through his master password.
|
// Ask the user to set the key through his master password.
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
if ([MPAppDelegate get].key)
|
if ([MPAppDelegate get].key)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
NSAlert *alert = [NSAlert alertWithMessageText:@"Master Password is locked."
|
NSAlert *alert = [NSAlert alertWithMessageText:@"Master Password is locked."
|
||||||
defaultButton:@"Unlock" alternateButton:@"Change" otherButton:@"Quit"
|
defaultButton:@"Unlock" alternateButton:@"Change" otherButton:@"Quit"
|
||||||
informativeTextWithFormat:@"Your master password is required to unlock the application."];
|
informativeTextWithFormat:@"Your master password is required to unlock the application."];
|
||||||
NSSecureTextField *passwordField = [[NSSecureTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 22)];
|
NSSecureTextField *passwordField = [[NSSecureTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 22)];
|
||||||
[alert setAccessoryView:passwordField];
|
[alert setAccessoryView:passwordField];
|
||||||
[alert layout];
|
[alert layout];
|
||||||
@@ -80,19 +80,20 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo {
|
- (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo {
|
||||||
|
|
||||||
switch (returnCode) {
|
switch (returnCode) {
|
||||||
case NSAlertAlternateReturn:
|
case NSAlertAlternateReturn:
|
||||||
// "Change" button.
|
// "Change" button.
|
||||||
if ([[NSAlert alertWithMessageText:@"Changing Master Password"
|
if ([[NSAlert alertWithMessageText:@"Changing Master Password"
|
||||||
defaultButton:nil alternateButton:[PearlStrings get].commonButtonCancel otherButton:nil
|
defaultButton:nil alternateButton:[PearlStrings get].commonButtonCancel otherButton:nil
|
||||||
informativeTextWithFormat:
|
informativeTextWithFormat:
|
||||||
@"This will allow you to log in with a different master password.\n\n"
|
@"This will allow you to log in with a different master password.\n\n"
|
||||||
@"Note that you will only see the sites and passwords for the master password you log in with.\n"
|
@"Note that you will only see the sites and passwords for the master password you log in with.\n"
|
||||||
@"If you log in with a different master password, your current sites will be unavailable.\n\n"
|
@"If you log in with a different master password, your current sites will be unavailable.\n\n"
|
||||||
@"You can always change back to your current master password later.\n"
|
@"You can always change back to your current master password later.\n"
|
||||||
@"Your current sites and passwords will then become available again."] runModal] == 1)
|
@"Your current sites and passwords will then become available again."] runModal]
|
||||||
|
== 1)
|
||||||
[[MPAppDelegate get] forgetKey];
|
[[MPAppDelegate get] forgetKey];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -107,23 +108,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index {
|
- (NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words
|
||||||
|
forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index {
|
||||||
|
|
||||||
NSString *query = [[control stringValue] substringWithRange:charRange];
|
NSString *query = [[control stringValue] substringWithRange:charRange];
|
||||||
if (![query length] || ![MPAppDelegate get].keyID)
|
if (![query length] || ![MPAppDelegate get].keyID)
|
||||||
return nil;
|
return nil;
|
||||||
|
|
||||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
||||||
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]];
|
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses_" ascending:NO]];
|
||||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND keyID == %@",
|
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND user == %@",
|
||||||
query, query, [MPAppDelegate get].keyID];
|
query, query, [MPAppDelegate get].activeUser];
|
||||||
|
|
||||||
NSError *error = nil;
|
NSError *error = nil;
|
||||||
self.siteResults = [[MPAppDelegate managedObjectContext] executeFetchRequest:fetchRequest error:&error];
|
self.siteResults = [[MPAppDelegate managedObjectContext] executeFetchRequest:fetchRequest error:&error];
|
||||||
if (error)
|
if (error)
|
||||||
err(@"Couldn't fetch elements: %@", error);
|
err(@"Couldn't fetch elements: %@", error);
|
||||||
|
|
||||||
NSMutableArray *mutableResults = [NSMutableArray arrayWithCapacity:[self.siteResults count] + 1];
|
NSMutableArray *mutableResults = [NSMutableArray arrayWithCapacity:[self.siteResults count] + 1];
|
||||||
if (self.siteResults)
|
if (self.siteResults)
|
||||||
for (MPElementEntity *element in self.siteResults)
|
for (MPElementEntity *element in self.siteResults)
|
||||||
[mutableResults addObject:element.name];
|
[mutableResults addObject:element.name];
|
||||||
@@ -153,7 +155,7 @@
|
|||||||
[[self findElement] use];
|
[[self findElement] use];
|
||||||
return YES;
|
return YES;
|
||||||
} else
|
} else
|
||||||
wrn(@"Couldn't copy password to pasteboard.");
|
wrn(@"Couldn't copy password to pasteboard.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return NO;
|
return NO;
|
||||||
@@ -175,19 +177,19 @@
|
|||||||
_content = content;
|
_content = content;
|
||||||
|
|
||||||
NSShadow *shadow = [NSShadow new];
|
NSShadow *shadow = [NSShadow new];
|
||||||
shadow.shadowColor = [NSColor colorWithDeviceWhite:0.0f alpha:0.6f];
|
shadow.shadowColor = [NSColor colorWithDeviceWhite:0.0f alpha:0.6f];
|
||||||
shadow.shadowOffset = NSMakeSize(1.0f, -1.0f);
|
shadow.shadowOffset = NSMakeSize(1.0f, -1.0f);
|
||||||
shadow.shadowBlurRadius = 1.2f;
|
shadow.shadowBlurRadius = 1.2f;
|
||||||
|
|
||||||
NSMutableParagraphStyle *paragraph = [NSMutableParagraphStyle new];
|
NSMutableParagraphStyle *paragraph = [NSMutableParagraphStyle new];
|
||||||
paragraph.alignment = NSCenterTextAlignment;
|
paragraph.alignment = NSCenterTextAlignment;
|
||||||
|
|
||||||
[self.contentField setAttributedStringValue:
|
[self.contentField setAttributedStringValue:
|
||||||
[[NSAttributedString alloc] initWithString:_content
|
[[NSAttributedString alloc] initWithString:_content
|
||||||
attributes:[[NSMutableDictionary alloc] initWithObjectsAndKeys:
|
attributes:[[NSMutableDictionary alloc] initWithObjectsAndKeys:
|
||||||
shadow, NSShadowAttributeName,
|
shadow, NSShadowAttributeName,
|
||||||
paragraph, NSParagraphStyleAttributeName,
|
paragraph, NSParagraphStyleAttributeName,
|
||||||
nil]]];
|
nil]]];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)trySite {
|
- (BOOL)trySite {
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#ifdef __OBJC__
|
#ifdef __OBJC__
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#import "Pearl-Prefix.pch"
|
#import "Pearl-Prefix.pch"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[]) {
|
||||||
{
|
|
||||||
return NSApplicationMain(argc, (const char **)argv);
|
return NSApplicationMain(argc, (const char **)argv);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>_XCCurrentVersionName</key>
|
<key>_XCCurrentVersionName</key>
|
||||||
<string>MasterPassword.xcdatamodel</string>
|
<string>MasterPassword 2.xcdatamodel</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<model name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1171" systemVersion="11E53" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
|
||||||
|
<entity name="MPElementEntity" representedClassName="MPElementEntity" isAbstract="YES" syncable="YES">
|
||||||
|
<attribute name="content" optional="YES" transient="YES" attributeType="Transformable" syncable="YES"/>
|
||||||
|
<attribute name="lastUsed" attributeType="Date" syncable="YES"/>
|
||||||
|
<attribute name="name" attributeType="String" minValueString="1" indexed="YES" syncable="YES"/>
|
||||||
|
<attribute name="type_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
|
||||||
|
<attribute name="uses_" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
|
||||||
|
<relationship name="user" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="MPUserEntity" inverseName="elements" inverseEntity="MPUserEntity" syncable="YES"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="MPElementGeneratedEntity" representedClassName="MPElementGeneratedEntity" parentEntity="MPElementEntity" syncable="YES">
|
||||||
|
<attribute name="counter_" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="MPElementStoredEntity" representedClassName="MPElementStoredEntity" parentEntity="MPElementEntity" syncable="YES">
|
||||||
|
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="MPUserEntity" representedClassName="MPUserEntity" syncable="YES">
|
||||||
|
<attribute name="avatar_" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
|
||||||
|
<attribute name="defaultType_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
|
||||||
|
<attribute name="keyID" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||||
|
<attribute name="lastUsed" optional="YES" attributeType="Date" syncable="YES"/>
|
||||||
|
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="saveKey_" attributeType="Boolean" defaultValueString="NO"/>
|
||||||
|
<relationship name="elements" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="MPElementEntity" inverseName="user" inverseEntity="MPElementEntity" syncable="YES"/>
|
||||||
|
</entity>
|
||||||
|
<elements>
|
||||||
|
<element name="MPElementEntity" positionX="160" positionY="192" width="128" height="135"/>
|
||||||
|
<element name="MPElementGeneratedEntity" positionX="160" positionY="192" width="128" height="60"/>
|
||||||
|
<element name="MPElementStoredEntity" positionX="160" positionY="192" width="128" height="60"/>
|
||||||
|
<element name="MPUserEntity" positionX="160" positionY="192" width="128" height="150"/>
|
||||||
|
</elements>
|
||||||
|
</model>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<model name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1487" systemVersion="12A269" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
|
||||||
|
<entity name="MPElementEntity" representedClassName="MPElementEntity" isAbstract="YES" syncable="YES">
|
||||||
|
<attribute name="content" optional="YES" transient="YES" attributeType="Transformable" syncable="YES"/>
|
||||||
|
<attribute name="lastUsed" attributeType="Date" indexed="YES" syncable="YES"/>
|
||||||
|
<attribute name="name" attributeType="String" minValueString="1" indexed="YES" syncable="YES"/>
|
||||||
|
<attribute name="requiresExplicitMigration_" attributeType="Boolean" defaultValueString="NO">
|
||||||
|
<userInfo/>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="type_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
|
||||||
|
<attribute name="userName" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="uses_" attributeType="Integer 16" defaultValueString="0" indexed="YES" syncable="YES"/>
|
||||||
|
<attribute name="version_" attributeType="Integer 16" minValueString="0" defaultValueString="0" syncable="YES"/>
|
||||||
|
<relationship name="user" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="MPUserEntity" inverseName="elements" inverseEntity="MPUserEntity" syncable="YES"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="MPElementGeneratedEntity" representedClassName="MPElementGeneratedEntity" parentEntity="MPElementEntity" syncable="YES">
|
||||||
|
<attribute name="counter_" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="MPElementStoredEntity" representedClassName="MPElementStoredEntity" parentEntity="MPElementEntity" syncable="YES">
|
||||||
|
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
|
||||||
|
</entity>
|
||||||
|
<entity name="MPUserEntity" representedClassName="MPUserEntity" syncable="YES">
|
||||||
|
<attribute name="avatar_" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
|
||||||
|
<attribute name="defaultType_" attributeType="Integer 16" defaultValueString="17" syncable="YES"/>
|
||||||
|
<attribute name="keyID" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||||
|
<attribute name="lastUsed" optional="YES" attributeType="Date" syncable="YES"/>
|
||||||
|
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="requiresExplicitMigration_" transient="YES" attributeType="Boolean" defaultValueString="NO" syncable="YES"/>
|
||||||
|
<attribute name="saveKey_" attributeType="Boolean" defaultValueString="NO">
|
||||||
|
<userInfo/>
|
||||||
|
</attribute>
|
||||||
|
<relationship name="elements" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="MPElementEntity" inverseName="user" inverseEntity="MPElementEntity" syncable="YES"/>
|
||||||
|
</entity>
|
||||||
|
<elements>
|
||||||
|
<element name="MPElementEntity" positionX="160" positionY="192" width="128" height="180"/>
|
||||||
|
<element name="MPElementGeneratedEntity" positionX="160" positionY="192" width="128" height="60"/>
|
||||||
|
<element name="MPElementStoredEntity" positionX="160" positionY="192" width="128" height="60"/>
|
||||||
|
<element name="MPUserEntity" positionX="160" positionY="192" width="128" height="150"/>
|
||||||
|
</elements>
|
||||||
|
</model>
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<model name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1171" systemVersion="11E53" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
|
|
||||||
<entity name="MPElementEntity" representedClassName="MPElementEntity" isAbstract="YES" syncable="YES">
|
|
||||||
<attribute name="keyID" attributeType="Binary" indexed="YES" syncable="YES" isSyncIdentityProperty="YES"/>
|
|
||||||
<attribute name="lastUsed" attributeType="Date" syncable="YES"/>
|
|
||||||
<attribute name="name" attributeType="String" minValueString="1" indexed="YES" syncable="YES" isSyncIdentityProperty="YES"/>
|
|
||||||
<attribute name="type" attributeType="Integer 16" defaultValueString="16" syncable="YES"/>
|
|
||||||
<attribute name="uses" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="MPElementGeneratedEntity" representedClassName="MPElementGeneratedEntity" parentEntity="MPElementEntity" syncable="YES">
|
|
||||||
<attribute name="counter" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<entity name="MPElementStoredEntity" representedClassName="MPElementStoredEntity" parentEntity="MPElementEntity" syncable="YES">
|
|
||||||
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
|
|
||||||
</entity>
|
|
||||||
<elements>
|
|
||||||
<element name="MPElementEntity" positionX="160" positionY="192" width="128" height="120"/>
|
|
||||||
<element name="MPElementGeneratedEntity" positionX="160" positionY="192" width="128" height="60"/>
|
|
||||||
<element name="MPElementStoredEntity" positionX="160" positionY="192" width="128" height="60"/>
|
|
||||||
</elements>
|
|
||||||
</model>
|
|
||||||
@@ -10,13 +10,14 @@
|
|||||||
#import <MessageUI/MessageUI.h>
|
#import <MessageUI/MessageUI.h>
|
||||||
#import "MPAppDelegate_Shared.h"
|
#import "MPAppDelegate_Shared.h"
|
||||||
|
|
||||||
@interface MPAppDelegate : MPAppDelegate_Shared <MFMailComposeViewControllerDelegate>
|
@interface MPAppDelegate : MPAppDelegate_Shared<MFMailComposeViewControllerDelegate>
|
||||||
|
|
||||||
+ (MPAppDelegate *)get;
|
+ (MPAppDelegate *)get;
|
||||||
|
|
||||||
|
- (void)checkConfig;
|
||||||
- (void)showGuide;
|
- (void)showGuide;
|
||||||
- (void)loadKey:(BOOL)animated;
|
|
||||||
|
|
||||||
- (void)export;
|
- (void)export;
|
||||||
|
- (void)changeMasterPasswordFor:(MPUserEntity *)user didResetBlock:(void(^)(void))didReset;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -10,21 +10,18 @@
|
|||||||
#import "MPAppDelegate_Key.h"
|
#import "MPAppDelegate_Key.h"
|
||||||
#import "MPAppDelegate_Store.h"
|
#import "MPAppDelegate_Store.h"
|
||||||
|
|
||||||
#import "MPMainViewController.h"
|
|
||||||
#import "IASKSettingsReader.h"
|
#import "IASKSettingsReader.h"
|
||||||
#import "LocalyticsSession.h"
|
#import "LocalyticsSession.h"
|
||||||
#import "TestFlight.h"
|
|
||||||
#import <Crashlytics/Crashlytics.h>
|
|
||||||
|
|
||||||
@interface MPAppDelegate ()
|
@interface MPAppDelegate ()
|
||||||
|
|
||||||
- (NSString *)testFlightInfo;
|
- (NSDictionary *)testFlightInfo;
|
||||||
- (NSString *)testFlightToken;
|
- (NSString *)testFlightToken;
|
||||||
|
|
||||||
- (NSString *)crashlyticsInfo;
|
- (NSDictionary *)crashlyticsInfo;
|
||||||
- (NSString *)crashlyticsAPIKey;
|
- (NSString *)crashlyticsAPIKey;
|
||||||
|
|
||||||
- (NSString *)localyticsInfo;
|
- (NSDictionary *)localyticsInfo;
|
||||||
- (NSString *)localyticsKey;
|
- (NSString *)localyticsKey;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -37,7 +34,7 @@
|
|||||||
[MPiOSConfig get];
|
[MPiOSConfig get];
|
||||||
|
|
||||||
#ifdef DEBUG
|
#ifdef DEBUG
|
||||||
[PearlLogger get].autoprintLevel = PearlLogLevelDebug;
|
[PearlLogger get].printLevel = PearlLogLevelDebug;
|
||||||
//[NSClassFromString(@"WebView") performSelector:NSSelectorFromString(@"_enableRemoteInspector")];
|
//[NSClassFromString(@"WebView") performSelector:NSSelectorFromString(@"_enableRemoteInspector")];
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -47,189 +44,119 @@
|
|||||||
return (MPAppDelegate *)[super get];
|
return (MPAppDelegate *)[super get];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)showGuide {
|
|
||||||
|
|
||||||
[self.navigationController performSegueWithIdentifier:@"MP_Guide" sender:self];
|
|
||||||
|
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointShowGuide];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)loadKey:(BOOL)animated {
|
|
||||||
|
|
||||||
if (!self.key)
|
|
||||||
// Try and load the key from the keychain.
|
|
||||||
[self loadStoredKey];
|
|
||||||
|
|
||||||
if (!self.key)
|
|
||||||
// Ask the user to set the key through his master password.
|
|
||||||
if ([NSThread isMainThread])
|
|
||||||
[self.navigationController presentViewController:
|
|
||||||
[self.navigationController.storyboard instantiateViewControllerWithIdentifier:@"MPUnlockViewController"]
|
|
||||||
animated:animated completion:nil];
|
|
||||||
else
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[self.navigationController presentViewController:
|
|
||||||
[self.navigationController.storyboard instantiateViewControllerWithIdentifier:@"MPUnlockViewController"]
|
|
||||||
animated:animated completion:nil];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)export {
|
|
||||||
|
|
||||||
[PearlAlert showNotice:
|
|
||||||
@"This will export all your site names.\n\n"
|
|
||||||
@"You can open the export with a text editor to get an overview of all your sites.\n\n"
|
|
||||||
@"The file also acts as a personal backup of your site list in case you don't sync with iCloud/iTunes."
|
|
||||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
|
||||||
[PearlAlert showAlertWithTitle:@"Reveal Passwords?" message:
|
|
||||||
@"Would you like to make all your passwords visible in the export?\n\n"
|
|
||||||
@"A safe export will only include your stored passwords, in an encrypted manner, "
|
|
||||||
@"making the result safe from falling in the wrong hands.\n\n"
|
|
||||||
@"If all your passwords are shown and somebody else finds the export, "
|
|
||||||
@"they could gain access to all your sites!"
|
|
||||||
viewStyle:UIAlertViewStyleDefault tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
|
||||||
if (buttonIndex == [alert firstOtherButtonIndex] + 0)
|
|
||||||
// Safe Export
|
|
||||||
[self exportShowPasswords:NO];
|
|
||||||
if (buttonIndex == [alert firstOtherButtonIndex] + 1)
|
|
||||||
// Safe Export
|
|
||||||
[self exportShowPasswords:YES];
|
|
||||||
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Safe Export", @"Show Passwords", nil];
|
|
||||||
} otherTitles:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)exportShowPasswords:(BOOL)showPasswords {
|
|
||||||
|
|
||||||
NSString *exportedSites = [self exportSitesShowingPasswords:showPasswords];
|
|
||||||
NSString *message;
|
|
||||||
if (showPasswords)
|
|
||||||
message = @"Export of my Master Password sites with passwords visible.\n\nREMINDER: Make sure nobody else sees this file!\n";
|
|
||||||
else
|
|
||||||
message = @"Backup of my Master Password sites.\n";
|
|
||||||
|
|
||||||
NSDateFormatter *exportDateFormatter = [NSDateFormatter new];
|
|
||||||
[exportDateFormatter setDateFormat:@"'Master Password sites ('yyyy'-'MM'-'DD').mpsites'"];
|
|
||||||
|
|
||||||
MFMailComposeViewController *composer = [[MFMailComposeViewController alloc] init];
|
|
||||||
[composer setMailComposeDelegate:self];
|
|
||||||
[composer setSubject:@"Master Password site export"];
|
|
||||||
[composer setMessageBody:message isHTML:NO];
|
|
||||||
[composer addAttachmentData:[exportedSites dataUsingEncoding:NSUTF8StringEncoding] mimeType:@"text/plain"
|
|
||||||
fileName:[exportDateFormatter stringFromDate:[NSDate date]]];
|
|
||||||
[self.window.rootViewController presentModalViewController:composer animated:YES];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - PearlConfigDelegate
|
|
||||||
|
|
||||||
- (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)value {
|
|
||||||
|
|
||||||
[self checkConfig];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)checkConfig {
|
|
||||||
|
|
||||||
if ([[MPConfig get].saveKey boolValue]) {
|
|
||||||
if (self.key)
|
|
||||||
[self updateKey:self.key];
|
|
||||||
} else
|
|
||||||
[self loadStoredKey];
|
|
||||||
|
|
||||||
if ([[MPConfig get].iCloud boolValue] != [self.storeManager iCloudEnabled])
|
|
||||||
[self.storeManager useiCloudStore:[[MPConfig get].iCloud boolValue] alertUser:YES];
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - UIApplicationDelegate
|
|
||||||
|
|
||||||
|
|
||||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||||
|
|
||||||
#ifndef DEBUG
|
[[[NSBundle mainBundle] mutableInfoDictionary] setObject:@"Master Password" forKey:@"CFBundleDisplayName"];
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
|
[[[NSBundle mainBundle] mutableLocalizedInfoDictionary] setObject:@"Master Password" forKey:@"CFBundleDisplayName"];
|
||||||
@try {
|
|
||||||
NSString *testFlightToken = [self testFlightToken];
|
|
||||||
if ([testFlightToken length]) {
|
|
||||||
dbg(@"Initializing TestFlight");
|
|
||||||
[TestFlight addCustomEnvironmentInformation:@"Anonymous" forKey:@"username"];
|
|
||||||
[TestFlight setOptions:[NSDictionary dictionaryWithObjectsAndKeys:
|
|
||||||
[NSNumber numberWithBool:NO], @"logToConsole",
|
|
||||||
[NSNumber numberWithBool:NO], @"logToSTDERR",
|
|
||||||
nil]];
|
|
||||||
[TestFlight takeOff:testFlightToken];
|
|
||||||
[[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) {
|
|
||||||
if (message.level >= PearlLogLevelInfo)
|
|
||||||
TFLog(@"%@", message);
|
|
||||||
|
|
||||||
return YES;
|
#ifdef ADHOC
|
||||||
}];
|
@try {
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointLaunched];
|
NSString *testFlightToken = [self testFlightToken];
|
||||||
}
|
if ([testFlightToken length]) {
|
||||||
}
|
inf(@"Initializing TestFlight");
|
||||||
@catch (NSException *exception) {
|
[TestFlight addCustomEnvironmentInformation:@"Anonymous" forKey:@"username"];
|
||||||
err(@"TestFlight: %@", exception);
|
[TestFlight setDeviceIdentifier:[(id)[UIDevice currentDevice] uniqueIdentifier]];
|
||||||
}
|
// [TestFlight setDeviceIdentifier:[PearlKeyChain deviceIdentifier]];
|
||||||
@try {
|
[TestFlight setOptions:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
NSString *crashlyticsAPIKey = [self crashlyticsAPIKey];
|
[NSNumber numberWithBool:NO], @"logToConsole",
|
||||||
if ([crashlyticsAPIKey length]) {
|
[NSNumber numberWithBool:NO], @"logToSTDERR",
|
||||||
dbg(@"Initializing Crashlytics");
|
nil]];
|
||||||
//[Crashlytics sharedInstance].debugMode = YES;
|
[TestFlight takeOff:testFlightToken];
|
||||||
[Crashlytics startWithAPIKey:crashlyticsAPIKey afterDelay:0];
|
[[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) {
|
||||||
}
|
PearlLogLevel level = PearlLogLevelWarn;
|
||||||
}
|
if ([[MPiOSConfig get].sendInfo boolValue])
|
||||||
@catch (NSException *exception) {
|
level = PearlLogLevelInfo;
|
||||||
err(@"Crashlytics: %@", exception);
|
|
||||||
}
|
|
||||||
@try {
|
|
||||||
NSString *localyticsKey = [self localyticsKey];
|
|
||||||
if ([localyticsKey length]) {
|
|
||||||
dbg(@"Initializing Localytics");
|
|
||||||
[[LocalyticsSession sharedLocalyticsSession] startSession:localyticsKey];
|
|
||||||
[[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) {
|
|
||||||
if (message.level >= PearlLogLevelError)
|
|
||||||
[[LocalyticsSession sharedLocalyticsSession] tagEvent:@"Problem" attributes:
|
|
||||||
[NSDictionary dictionaryWithObjectsAndKeys:
|
|
||||||
[message levelDescription],
|
|
||||||
@"level",
|
|
||||||
message.message,
|
|
||||||
@"message",
|
|
||||||
nil]];
|
|
||||||
|
|
||||||
return YES;
|
if (message.level >= level)
|
||||||
}];
|
TFLog(@"%@", message);
|
||||||
}
|
|
||||||
|
return YES;
|
||||||
|
}];
|
||||||
|
TFLog(@"TestFlight (%@) initialized for: %@ v%@.", //
|
||||||
|
TESTFLIGHT_SDK_VERSION, [PearlInfoPlist get].CFBundleName, [PearlInfoPlist get].CFBundleVersion);
|
||||||
}
|
}
|
||||||
@catch (NSException *exception) {
|
}
|
||||||
err(@"Localytics exception: %@", exception);
|
@catch (id exception) {
|
||||||
}
|
err(@"TestFlight: %@", exception);
|
||||||
});
|
}
|
||||||
#endif
|
#endif
|
||||||
|
@try {
|
||||||
|
NSString *crashlyticsAPIKey = [self crashlyticsAPIKey];
|
||||||
|
if ([crashlyticsAPIKey length]) {
|
||||||
|
inf(@"Initializing Crashlytics");
|
||||||
|
#if defined (DEBUG) || defined (ADHOC)
|
||||||
|
[Crashlytics sharedInstance].debugMode = YES;
|
||||||
|
#endif
|
||||||
|
[[Crashlytics sharedInstance] setObjectValue:@"Anonymous" forKey:@"username"];
|
||||||
|
[[Crashlytics sharedInstance] setObjectValue:[PearlKeyChain deviceIdentifier] forKey:@"deviceIdentifier"];
|
||||||
|
[Crashlytics startWithAPIKey:crashlyticsAPIKey afterDelay:0];
|
||||||
|
[[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) {
|
||||||
|
PearlLogLevel level = PearlLogLevelWarn;
|
||||||
|
if ([[MPiOSConfig get].sendInfo boolValue])
|
||||||
|
level = PearlLogLevelInfo;
|
||||||
|
|
||||||
UIImage *navBarImage = [[UIImage imageNamed:@"ui_navbar_container"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 5)];
|
if (message.level >= level)
|
||||||
|
CLSLog(@"%@", message);
|
||||||
|
|
||||||
|
return YES;
|
||||||
|
}];
|
||||||
|
CLSLog(@"Crashlytics (%@) initialized for: %@ v%@.", //
|
||||||
|
[Crashlytics sharedInstance].version, [PearlInfoPlist get].CFBundleName, [PearlInfoPlist get].CFBundleVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@catch (id exception) {
|
||||||
|
err(@"Crashlytics: %@", exception);
|
||||||
|
}
|
||||||
|
@try {
|
||||||
|
NSString *localyticsKey = [self localyticsKey];
|
||||||
|
if ([localyticsKey length]) {
|
||||||
|
inf(@"Initializing Localytics");
|
||||||
|
[[LocalyticsSession sharedLocalyticsSession] startSession:localyticsKey];
|
||||||
|
[[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) {
|
||||||
|
if (message.level >= PearlLogLevelWarn)
|
||||||
|
[[LocalyticsSession sharedLocalyticsSession] tagEvent:@"Problem"
|
||||||
|
attributes:[NSDictionary
|
||||||
|
dictionaryWithObjectsAndKeys:
|
||||||
|
[NSString stringWithCString:PearlLogLevelStr(message.level)
|
||||||
|
encoding:NSASCIIStringEncoding], @"level",
|
||||||
|
message.message, @"message",
|
||||||
|
nil]];
|
||||||
|
|
||||||
|
return YES;
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@catch (id exception) {
|
||||||
|
err(@"Localytics exception: %@", exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
UIImage *navBarImage = [[UIImage imageNamed:@"ui_navbar_container"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 5)];
|
||||||
[[UINavigationBar appearance] setBackgroundImage:navBarImage forBarMetrics:UIBarMetricsDefault];
|
[[UINavigationBar appearance] setBackgroundImage:navBarImage forBarMetrics:UIBarMetricsDefault];
|
||||||
[[UINavigationBar appearance] setBackgroundImage:navBarImage forBarMetrics:UIBarMetricsLandscapePhone];
|
[[UINavigationBar appearance] setBackgroundImage:navBarImage forBarMetrics:UIBarMetricsLandscapePhone];
|
||||||
[[UINavigationBar appearance] setTitleTextAttributes:
|
[[UINavigationBar appearance] setTitleTextAttributes:
|
||||||
[NSDictionary dictionaryWithObjectsAndKeys:
|
[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
[UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:1.0f], UITextAttributeTextColor,
|
[UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:1.0f], UITextAttributeTextColor,
|
||||||
[UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.8f], UITextAttributeTextShadowColor,
|
[UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.8f], UITextAttributeTextShadowColor,
|
||||||
[NSValue valueWithUIOffset:UIOffsetMake(0, -1)], UITextAttributeTextShadowOffset,
|
[NSValue valueWithUIOffset:UIOffsetMake(0, -1)], UITextAttributeTextShadowOffset,
|
||||||
[UIFont fontWithName:@"Exo-Bold" size:20.0f], UITextAttributeFont,
|
[UIFont fontWithName:@"Exo-Bold" size:20.0f], UITextAttributeFont,
|
||||||
nil]];
|
nil]];
|
||||||
|
|
||||||
UIImage *navBarButton = [[UIImage imageNamed:@"ui_navbar_button"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 5)];
|
UIImage *navBarButton = [[UIImage imageNamed:@"ui_navbar_button"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 5)];
|
||||||
UIImage *navBarBack = [[UIImage imageNamed:@"ui_navbar_back"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 13, 0, 5)];
|
UIImage *navBarBack = [[UIImage imageNamed:@"ui_navbar_back"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 13, 0, 5)];
|
||||||
[[UIBarButtonItem appearance] setBackgroundImage:navBarButton forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
[[UIBarButtonItem appearance] setBackgroundImage:navBarButton forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
||||||
[[UIBarButtonItem appearance] setBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];
|
[[UIBarButtonItem appearance] setBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];
|
||||||
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:navBarBack forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:navBarBack forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
||||||
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];
|
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];
|
||||||
[[UIBarButtonItem appearance] setTitleTextAttributes:
|
[[UIBarButtonItem appearance] setTitleTextAttributes:
|
||||||
[NSDictionary dictionaryWithObjectsAndKeys:
|
[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
[UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:1.0f], UITextAttributeTextColor,
|
[UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:1.0f], UITextAttributeTextColor,
|
||||||
[UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.5f], UITextAttributeTextShadowColor,
|
[UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.5f], UITextAttributeTextShadowColor,
|
||||||
[NSValue valueWithUIOffset:UIOffsetMake(0, 1)], UITextAttributeTextShadowOffset,
|
[NSValue valueWithUIOffset:UIOffsetMake(0, 1)], UITextAttributeTextShadowOffset,
|
||||||
[UIFont fontWithName:@"Helvetica-Neue" size:0.0f], UITextAttributeFont,
|
[UIFont fontWithName:@"Helvetica-Neue" size:0.0f], UITextAttributeFont,
|
||||||
nil]
|
nil]
|
||||||
forState:UIControlStateNormal];
|
forState:UIControlStateNormal];
|
||||||
|
|
||||||
UIImage *toolBarImage = [[UIImage imageNamed:@"ui_toolbar_container"] resizableImageWithCapInsets:UIEdgeInsetsMake(25, 5, 5, 5)];
|
UIImage *toolBarImage = [[UIImage imageNamed:@"ui_toolbar_container"] resizableImageWithCapInsets:UIEdgeInsetsMake(25, 5, 5, 5)];
|
||||||
[[UISearchBar appearance] setBackgroundImage:toolBarImage];
|
[[UISearchBar appearance] setBackgroundImage:toolBarImage];
|
||||||
[[UIToolbar appearance] setBackgroundImage:toolBarImage forToolbarPosition:UIToolbarPositionAny barMetrics:UIBarMetricsDefault];
|
[[UIToolbar appearance] setBackgroundImage:toolBarImage forToolbarPosition:UIToolbarPositionAny barMetrics:UIBarMetricsDefault];
|
||||||
|
|
||||||
@@ -253,8 +180,17 @@
|
|||||||
|
|
||||||
[[UISegmentedControl appearance] setDividerImage:segmentUnselectedUnselected forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
[[UISegmentedControl appearance] setDividerImage:segmentUnselectedUnselected forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
||||||
[[UISegmentedControl appearance] setDividerImage:segmentSelectedUnselected forLeftSegmentState:UIControlStateSelected rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
[[UISegmentedControl appearance] setDividerImage:segmentSelectedUnselected forLeftSegmentState:UIControlStateSelected rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
||||||
[[UISegmentedControl appearance] setDividerImage:segUnselectedSelected forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateSelected barMetrics:UIBarMetricsDefault];*/
|
[[UISegmentedControl appearance] setDividerImage:segUnselectedSelected forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateSelected barMetrics:UIBarMetricsDefault];
|
||||||
|
*/
|
||||||
|
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserverForName:MPNotificationSignedOut object:nil queue:nil
|
||||||
|
usingBlock:^(NSNotification *note) {
|
||||||
|
if ([[note.userInfo objectForKey:@"animated"] boolValue])
|
||||||
|
[self.navigationController performSegueWithIdentifier:@"MP_Unlock" sender:nil];
|
||||||
|
else
|
||||||
|
[self.navigationController presentViewController:[self.navigationController.storyboard instantiateViewControllerWithIdentifier:@"MPUnlockViewController"]
|
||||||
|
animated:NO completion:nil];
|
||||||
|
}];
|
||||||
[[NSNotificationCenter defaultCenter] addObserverForName:kIASKAppSettingChanged object:nil queue:nil
|
[[NSNotificationCenter defaultCenter] addObserverForName:kIASKAppSettingChanged object:nil queue:nil
|
||||||
usingBlock:^(NSNotification *note) {
|
usingBlock:^(NSNotification *note) {
|
||||||
[self checkConfig];
|
[self checkConfig];
|
||||||
@@ -268,32 +204,35 @@
|
|||||||
@"lhunath@lyndir.com\n"
|
@"lhunath@lyndir.com\n"
|
||||||
@"Or report detailed issues at:\n"
|
@"Or report detailed issues at:\n"
|
||||||
@"https://youtrack.lyndir.com\n"
|
@"https://youtrack.lyndir.com\n"
|
||||||
viewStyle:UIAlertViewStyleDefault tappedButtonBlock:nil
|
viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:nil
|
||||||
cancelTitle:nil otherTitles:[PearlStrings get].commonButtonOkay, nil];
|
cancelTitle:nil otherTitles:[PearlStrings get].commonButtonOkay, nil];
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
[[UIApplication sharedApplication] setStatusBarHidden:NO
|
[super application:application didFinishLaunchingWithOptions:launchOptions];
|
||||||
withAnimation:UIStatusBarAnimationSlide];
|
|
||||||
|
|
||||||
return [super application:application didFinishLaunchingWithOptions:launchOptions];
|
inf(@"Started up with device identifier: %@", [PearlKeyChain deviceIdentifier]);
|
||||||
|
[TestFlight passCheckpoint:MPCheckpointLaunched];
|
||||||
|
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointLaunched];
|
||||||
|
|
||||||
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
|
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
|
||||||
sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
|
sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
|
||||||
|
|
||||||
__autoreleasing NSError *error;
|
__autoreleasing NSError *error;
|
||||||
__autoreleasing NSURLResponse *response;
|
__autoreleasing NSURLResponse *response;
|
||||||
NSData *importedSitesData = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url]
|
NSData *importedSitesData = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url]
|
||||||
returningResponse:&response error:&error];
|
returningResponse:&response error:&error];
|
||||||
if (error)
|
if (error)
|
||||||
err(@"While reading imported sites from %@: %@", url, error);
|
err(@"While reading imported sites from %@: %@", url, error);
|
||||||
if (!importedSitesData)
|
if (!importedSitesData)
|
||||||
return NO;
|
return NO;
|
||||||
|
|
||||||
NSString *importedSitesString = [[NSString alloc] initWithData:importedSitesData encoding:NSUTF8StringEncoding];
|
NSString *importedSitesString = [[NSString alloc] initWithData:importedSitesData encoding:NSUTF8StringEncoding];
|
||||||
[PearlAlert showAlertWithTitle:@"Import Password" message:
|
[PearlAlert showAlertWithTitle:@"Import Password" message:
|
||||||
@"Enter the master password for this export:"
|
@"Enter the master password for this export:"
|
||||||
viewStyle:UIAlertViewStyleSecureTextInput tappedButtonBlock:
|
viewStyle:UIAlertViewStyleSecureTextInput initAlert:nil tappedButtonBlock:
|
||||||
^(UIAlertView *alert, NSInteger buttonIndex) {
|
^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
MPImportResult result = [self importSites:importedSitesString withPassword:[alert textFieldAtIndex:0].text
|
MPImportResult result = [self importSites:importedSitesString withPassword:[alert textFieldAtIndex:0].text
|
||||||
@@ -304,10 +243,14 @@
|
|||||||
dispatch_group_enter(confirmationGroup);
|
dispatch_group_enter(confirmationGroup);
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[PearlAlert showAlertWithTitle:@"Import Sites?"
|
[PearlAlert showAlertWithTitle:@"Import Sites?"
|
||||||
message:PearlLocalize(@"Import %d sites, overwriting %d existing sites?", importCount, deleteCount)
|
message:PearlString(
|
||||||
|
@"Import %d sites, overwriting %d existing sites?",
|
||||||
|
importCount, deleteCount)
|
||||||
viewStyle:UIAlertViewStyleDefault
|
viewStyle:UIAlertViewStyleDefault
|
||||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
initAlert:nil
|
||||||
if (buttonIndex != [alert cancelButtonIndex])
|
tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
|
||||||
|
if (buttonIndex_
|
||||||
|
!= [alert_ cancelButtonIndex])
|
||||||
confirmation = YES;
|
confirmation = YES;
|
||||||
|
|
||||||
dispatch_group_leave(confirmationGroup);
|
dispatch_group_leave(confirmationGroup);
|
||||||
@@ -315,7 +258,8 @@
|
|||||||
cancelTitle:[PearlStrings get].commonButtonCancel
|
cancelTitle:[PearlStrings get].commonButtonCancel
|
||||||
otherTitles:@"Import", nil];
|
otherTitles:@"Import", nil];
|
||||||
});
|
});
|
||||||
dispatch_group_wait(confirmationGroup, DISPATCH_TIME_FOREVER);
|
dispatch_group_wait(
|
||||||
|
confirmationGroup, DISPATCH_TIME_FOREVER);
|
||||||
|
|
||||||
return confirmation;
|
return confirmation;
|
||||||
}];
|
}];
|
||||||
@@ -341,16 +285,24 @@
|
|||||||
return YES;
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
|
||||||
|
|
||||||
|
wrn(@"Received memory warning.");
|
||||||
|
|
||||||
|
[super applicationDidReceiveMemoryWarning:application];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)applicationDidBecomeActive:(UIApplication *)application {
|
- (void)applicationDidBecomeActive:(UIApplication *)application {
|
||||||
|
|
||||||
|
inf(@"Re-activated");
|
||||||
|
[[MPAppDelegate get] checkConfig];
|
||||||
|
|
||||||
if ([[MPiOSConfig get].showQuickStart boolValue])
|
if ([[MPiOSConfig get].showQuickStart boolValue])
|
||||||
[self showGuide];
|
[self showGuide];
|
||||||
else {
|
|
||||||
[self loadKey:NO];
|
|
||||||
[self checkConfig];
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointActivated];
|
[TestFlight passCheckpoint:MPCheckpointActivated];
|
||||||
|
|
||||||
|
[super applicationDidBecomeActive:application];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
||||||
@@ -373,7 +325,7 @@
|
|||||||
|
|
||||||
[self saveContext];
|
[self saveContext];
|
||||||
|
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointTerminated];
|
[TestFlight passCheckpoint:MPCheckpointTerminated];
|
||||||
|
|
||||||
[[LocalyticsSession sharedLocalyticsSession] close];
|
[[LocalyticsSession sharedLocalyticsSession] close];
|
||||||
[[LocalyticsSession sharedLocalyticsSession] upload];
|
[[LocalyticsSession sharedLocalyticsSession] upload];
|
||||||
@@ -383,14 +335,191 @@
|
|||||||
|
|
||||||
- (void)applicationWillResignActive:(UIApplication *)application {
|
- (void)applicationWillResignActive:(UIApplication *)application {
|
||||||
|
|
||||||
|
inf(@"Will deactivate");
|
||||||
[self saveContext];
|
[self saveContext];
|
||||||
|
|
||||||
if (![[MPiOSConfig get].rememberKey boolValue]) {
|
if (![[MPiOSConfig get].rememberLogin boolValue])
|
||||||
[self updateKey:nil];
|
[self signOutAnimated:NO];
|
||||||
[self loadKey:NO];
|
|
||||||
|
[TestFlight passCheckpoint:MPCheckpointDeactivated];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - Behavior
|
||||||
|
|
||||||
|
- (void)checkConfig {
|
||||||
|
|
||||||
|
if ([[MPConfig get].iCloud boolValue] != [self.storeManager iCloudEnabled])
|
||||||
|
[self.storeManager useiCloudStore:[[MPConfig get].iCloud boolValue] alertUser:YES];
|
||||||
|
if ([[MPiOSConfig get].sendInfo boolValue]) {
|
||||||
|
if ([PearlLogger get].printLevel > PearlLogLevelInfo)
|
||||||
|
[PearlLogger get].printLevel = PearlLogLevelInfo;
|
||||||
|
|
||||||
|
[[Crashlytics sharedInstance] setBoolValue:[[MPConfig get].rememberLogin boolValue] forKey:@"rememberLogin"];
|
||||||
|
[[Crashlytics sharedInstance] setBoolValue:[[MPConfig get].iCloud boolValue] forKey:@"iCloud"];
|
||||||
|
[[Crashlytics sharedInstance] setBoolValue:[[MPConfig get].iCloudDecided boolValue] forKey:@"iCloudDecided"];
|
||||||
|
[[Crashlytics sharedInstance] setBoolValue:[[MPiOSConfig get].sendInfo boolValue] forKey:@"sendInfo"];
|
||||||
|
[[Crashlytics sharedInstance] setBoolValue:[[MPiOSConfig get].helpHidden boolValue] forKey:@"helpHidden"];
|
||||||
|
[[Crashlytics sharedInstance] setBoolValue:[[MPiOSConfig get].showQuickStart boolValue] forKey:@"showQuickStart"];
|
||||||
|
[[Crashlytics sharedInstance] setBoolValue:[[PearlConfig get].firstRun boolValue] forKey:@"firstRun"];
|
||||||
|
[[Crashlytics sharedInstance] setIntValue:[[PearlConfig get].launchCount intValue] forKey:@"launchCount"];
|
||||||
|
[[Crashlytics sharedInstance] setBoolValue:[[PearlConfig get].askForReviews boolValue] forKey:@"askForReviews"];
|
||||||
|
[[Crashlytics sharedInstance] setIntValue:[[PearlConfig get].reviewAfterLaunches intValue] forKey:@"reviewAfterLaunches"];
|
||||||
|
[[Crashlytics sharedInstance] setObjectValue:[PearlConfig get].reviewedVersion forKey:@"reviewedVersion"];
|
||||||
|
|
||||||
|
[TestFlight addCustomEnvironmentInformation:[[MPConfig get].rememberLogin boolValue]? @"YES": @"NO" forKey:@"rememberLogin"];
|
||||||
|
[TestFlight addCustomEnvironmentInformation:[[MPConfig get].iCloud boolValue]? @"YES": @"NO" forKey:@"iCloud"];
|
||||||
|
[TestFlight addCustomEnvironmentInformation:[[MPConfig get].iCloudDecided boolValue]? @"YES": @"NO" forKey:@"iCloudDecided"];
|
||||||
|
[TestFlight addCustomEnvironmentInformation:[[MPiOSConfig get].sendInfo boolValue]? @"YES": @"NO" forKey:@"sendInfo"];
|
||||||
|
[TestFlight addCustomEnvironmentInformation:[[MPiOSConfig get].helpHidden boolValue]? @"YES": @"NO" forKey:@"helpHidden"];
|
||||||
|
[TestFlight addCustomEnvironmentInformation:[[MPiOSConfig get].showQuickStart boolValue]? @"YES": @"NO" forKey:@"showQuickStart"];
|
||||||
|
[TestFlight addCustomEnvironmentInformation:[[PearlConfig get].firstRun boolValue]? @"YES": @"NO" forKey:@"firstRun"];
|
||||||
|
[TestFlight addCustomEnvironmentInformation:[[PearlConfig get].launchCount description] forKey:@"launchCount"];
|
||||||
|
[TestFlight addCustomEnvironmentInformation:[[PearlConfig get].askForReviews boolValue]? @"YES": @"NO" forKey:@"askForReviews"];
|
||||||
|
[TestFlight addCustomEnvironmentInformation:[[PearlConfig get].reviewAfterLaunches description] forKey:@"reviewAfterLaunches"];
|
||||||
|
[TestFlight addCustomEnvironmentInformation:[PearlConfig get].reviewedVersion forKey:@"reviewedVersion"];
|
||||||
|
|
||||||
|
[TestFlight passCheckpoint:MPCheckpointConfig];
|
||||||
|
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointConfig attributes:
|
||||||
|
[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
|
[[MPConfig get].rememberLogin boolValue]
|
||||||
|
? @"YES": @"NO", @"rememberLogin",
|
||||||
|
[[MPConfig get].iCloud boolValue]? @"YES"
|
||||||
|
: @"NO", @"iCloud",
|
||||||
|
[[MPConfig get].iCloudDecided boolValue]
|
||||||
|
? @"YES": @"NO", @"iCloudDecided",
|
||||||
|
[[MPiOSConfig get].sendInfo boolValue]
|
||||||
|
? @"YES": @"NO", @"sendInfo",
|
||||||
|
[[MPiOSConfig get].helpHidden boolValue]
|
||||||
|
? @"YES": @"NO", @"helpHidden",
|
||||||
|
[[MPiOSConfig get].showQuickStart boolValue]
|
||||||
|
? @"YES": @"NO", @"showQuickStart",
|
||||||
|
[[PearlConfig get].firstRun boolValue]
|
||||||
|
? @"YES": @"NO", @"firstRun",
|
||||||
|
[[PearlConfig get].launchCount description],
|
||||||
|
@"launchCount",
|
||||||
|
[[PearlConfig get].askForReviews boolValue]
|
||||||
|
? @"YES": @"NO", @"askForReviews",
|
||||||
|
[[PearlConfig get].reviewAfterLaunches description],
|
||||||
|
@"reviewAfterLaunches",
|
||||||
|
[PearlConfig get].reviewedVersion,
|
||||||
|
@"reviewedVersion",
|
||||||
|
nil]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)showGuide {
|
||||||
|
|
||||||
|
[self.navigationController performSegueWithIdentifier:@"MP_Guide" sender:self];
|
||||||
|
|
||||||
|
[TestFlight passCheckpoint:MPCheckpointShowGuide];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)export {
|
||||||
|
|
||||||
|
[PearlAlert showNotice:
|
||||||
|
@"This will export all your site names.\n\n"
|
||||||
|
@"You can open the export with a text editor to get an overview of all your sites.\n\n"
|
||||||
|
@"The file also acts as a personal backup of your site list in case you don't sync with iCloud/iTunes."
|
||||||
|
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||||
|
[PearlAlert showAlertWithTitle:@"Reveal Passwords?" message:
|
||||||
|
@"Would you like to make all your passwords visible in the export?\n\n"
|
||||||
|
@"A safe export will only include your stored passwords, in an encrypted manner, "
|
||||||
|
@"making the result safe from falling in the wrong hands.\n\n"
|
||||||
|
@"If all your passwords are shown and somebody else finds the export, "
|
||||||
|
@"they could gain access to all your sites!"
|
||||||
|
viewStyle:UIAlertViewStyleDefault initAlert:nil
|
||||||
|
tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
|
||||||
|
if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 0)
|
||||||
|
// Safe Export
|
||||||
|
[self exportShowPasswords:NO];
|
||||||
|
if (buttonIndex_ == [alert_ firstOtherButtonIndex] + 1)
|
||||||
|
// Show Passwords
|
||||||
|
[self exportShowPasswords:YES];
|
||||||
|
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Safe Export", @"Show Passwords", nil];
|
||||||
|
} otherTitles:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)exportShowPasswords:(BOOL)showPasswords {
|
||||||
|
|
||||||
|
if (![MFMailComposeViewController canSendMail]) {
|
||||||
|
[PearlAlert showAlertWithTitle:@"Cannot Send Mail"
|
||||||
|
message:
|
||||||
|
@"Your device is not yet set up for sending mail.\n"
|
||||||
|
@"Close Master Password, go into Settings and add a Mail account."
|
||||||
|
viewStyle:UIAlertViewStyleDefault
|
||||||
|
initAlert:nil tappedButtonBlock:nil cancelTitle:[PearlStrings get].commonButtonOkay
|
||||||
|
otherTitles:nil];
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointDeactivated];
|
NSString *exportedSites = [self exportSitesShowingPasswords:showPasswords];
|
||||||
|
NSString *message;
|
||||||
|
|
||||||
|
if (showPasswords)
|
||||||
|
message = PearlString(@"Export of Master Password sites with passwords included.\n"
|
||||||
|
@"REMINDER: Make sure nobody else sees this file! Passwords are visible!\n\n\n"
|
||||||
|
@"--\n"
|
||||||
|
@"%@\n"
|
||||||
|
@"Master Password %@, build %@",
|
||||||
|
self.activeUser.name,
|
||||||
|
[PearlInfoPlist get].CFBundleShortVersionString,
|
||||||
|
[PearlInfoPlist get].CFBundleVersion);
|
||||||
|
else
|
||||||
|
message = PearlString(@"Backup of Master Password sites.\n\n\n"
|
||||||
|
@"--\n"
|
||||||
|
@"%@\n"
|
||||||
|
@"Master Password %@, build %@",
|
||||||
|
self.activeUser.name,
|
||||||
|
[PearlInfoPlist get].CFBundleShortVersionString,
|
||||||
|
[PearlInfoPlist get].CFBundleVersion);
|
||||||
|
|
||||||
|
NSDateFormatter *exportDateFormatter = [NSDateFormatter new];
|
||||||
|
[exportDateFormatter setDateFormat:@"yyyy'-'MM'-'dd"];
|
||||||
|
|
||||||
|
MFMailComposeViewController *composer = [MFMailComposeViewController new];
|
||||||
|
[composer setMailComposeDelegate:self];
|
||||||
|
[composer setSubject:@"Master Password Export"];
|
||||||
|
[composer setMessageBody:message isHTML:NO];
|
||||||
|
[composer addAttachmentData:
|
||||||
|
[exportedSites dataUsingEncoding:NSUTF8StringEncoding] mimeType:@"text/plain"
|
||||||
|
fileName:PearlString(@"%@ (%@).mpsites",
|
||||||
|
self.activeUser.name,
|
||||||
|
[exportDateFormatter stringFromDate:[NSDate date]])];
|
||||||
|
[self.window.rootViewController presentModalViewController:composer animated:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)changeMasterPasswordFor:(MPUserEntity *)user didResetBlock:(void (^)(void))didReset {
|
||||||
|
|
||||||
|
[PearlAlert showAlertWithTitle:@"Changing Master Password"
|
||||||
|
message:
|
||||||
|
@"If you continue, you'll be able to set a new master password.\n\n"
|
||||||
|
@"Changing your master password will cause all your generated passwords to change!\n"
|
||||||
|
@"Changing the master password back to the old one will cause your passwords to revert as well."
|
||||||
|
viewStyle:UIAlertViewStyleDefault
|
||||||
|
initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||||
|
if (buttonIndex == [alert cancelButtonIndex])
|
||||||
|
return;
|
||||||
|
|
||||||
|
inf(@"Unsetting master password for: %@.", user.userID);
|
||||||
|
user.keyID = nil;
|
||||||
|
[self forgetSavedKeyFor:user];
|
||||||
|
[self signOutAnimated:YES];
|
||||||
|
|
||||||
|
if (didReset)
|
||||||
|
didReset();
|
||||||
|
|
||||||
|
[TestFlight passCheckpoint:MPCheckpointChangeMP];
|
||||||
|
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointChangeMP attributes:nil];
|
||||||
|
}
|
||||||
|
cancelTitle:[PearlStrings get].commonButtonAbort
|
||||||
|
otherTitles:[PearlStrings get].commonButtonContinue, nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - PearlConfigDelegate
|
||||||
|
|
||||||
|
- (void)didUpdateConfigForKey:(SEL)configKey fromValue:(id)value {
|
||||||
|
|
||||||
|
[self checkConfig];
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - MFMailComposeViewControllerDelegate
|
#pragma mark - MFMailComposeViewControllerDelegate
|
||||||
@@ -399,7 +528,7 @@
|
|||||||
didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error {
|
didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error {
|
||||||
|
|
||||||
if (error)
|
if (error)
|
||||||
err(@"Error composing mail message: %@", error);
|
err(@"Error composing mail message: %@", error);
|
||||||
|
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case MFMailComposeResultSaved:
|
case MFMailComposeResultSaved:
|
||||||
@@ -407,10 +536,11 @@
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case MFMailComposeResultFailed:
|
case MFMailComposeResultFailed:
|
||||||
[PearlAlert showError:@"A problem occurred while sending the message." tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
[PearlAlert showError:@"A problem occurred while sending the message."
|
||||||
if (buttonIndex == [alert firstOtherButtonIndex])
|
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||||
return;
|
if (buttonIndex == [alert firstOtherButtonIndex])
|
||||||
} otherTitles:@"Retry", nil];
|
return;
|
||||||
|
} otherTitles:@"Retry", nil];
|
||||||
return;
|
return;
|
||||||
case MFMailComposeResultCancelled:
|
case MFMailComposeResultCancelled:
|
||||||
break;
|
break;
|
||||||
@@ -429,37 +559,38 @@
|
|||||||
if (!iCloudEnabled) {
|
if (!iCloudEnabled) {
|
||||||
[PearlAlert showAlertWithTitle:@"iCloud"
|
[PearlAlert showAlertWithTitle:@"iCloud"
|
||||||
message:
|
message:
|
||||||
@"iCloud is now disabled.\n\n"
|
@"iCloud is now disabled.\n\n"
|
||||||
@"It is highly recommended you enable iCloud."
|
@"It is highly recommended you enable iCloud."
|
||||||
viewStyle:UIAlertViewStyleDefault tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
viewStyle:UIAlertViewStyleDefault initAlert:nil
|
||||||
if (buttonIndex == [alert firstOtherButtonIndex] + 0) {
|
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||||
[PearlAlert showAlertWithTitle:@"About iCloud"
|
if (buttonIndex == [alert firstOtherButtonIndex] + 0) {
|
||||||
message:
|
[PearlAlert showAlertWithTitle:@"About iCloud"
|
||||||
@"iCloud is Apple's solution for saving your data in \"the cloud\" "
|
message:
|
||||||
@"and making sure your other iPhones, iPads and Macs are in sync.\n\n"
|
@"iCloud is Apple's solution for saving your data in \"the cloud\" "
|
||||||
@"For Master Password, that means your sites are available on all your "
|
@"and making sure your other iPhones, iPads and Macs are in sync.\n\n"
|
||||||
@"Apple devices, and you always have a backup of them in case "
|
@"For Master Password, that means your sites are available on all your "
|
||||||
@"you loose one or need to restore.\n\n"
|
@"Apple devices, and you always have a backup of them in case "
|
||||||
@"Because of the way Master Password works, it doesn't need to send your "
|
@"you loose one or need to restore.\n\n"
|
||||||
@"site's passwords to Apple. Only their names are saved to make it easier "
|
@"Because of the way Master Password works, it doesn't need to send your "
|
||||||
@"for you to find the site you need. For some sites you may have set "
|
@"site's passwords to Apple. Only their names are saved to make it easier "
|
||||||
@"a user-specified password: these are sent to iCloud after being encrypted "
|
@"for you to find the site you need. For some sites you may have set "
|
||||||
@"with your master password.\n\n"
|
@"a user-specified password: these are sent to iCloud after being encrypted "
|
||||||
@"Apple can never see any of your passwords."
|
@"with your master password.\n\n"
|
||||||
viewStyle:UIAlertViewStyleDefault
|
@"Apple can never see any of your passwords."
|
||||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
viewStyle:UIAlertViewStyleDefault
|
||||||
[self ubiquityStoreManager:manager didSwitchToiCloud:iCloudEnabled];
|
initAlert:nil tappedButtonBlock:^(UIAlertView *alert_, NSInteger buttonIndex_) {
|
||||||
}
|
[self ubiquityStoreManager:manager didSwitchToiCloud:iCloudEnabled];
|
||||||
cancelTitle:[PearlStrings get].commonButtonThanks otherTitles:nil];
|
}
|
||||||
return;
|
cancelTitle:[PearlStrings get].commonButtonThanks otherTitles:nil];
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
[MPConfig get].iCloudDecided = [NSNumber numberWithBool:YES];
|
[MPConfig get].iCloudDecided = [NSNumber numberWithBool:YES];
|
||||||
if (buttonIndex == [alert cancelButtonIndex])
|
if (buttonIndex == [alert cancelButtonIndex])
|
||||||
return;
|
return;
|
||||||
if (buttonIndex == [alert firstOtherButtonIndex] + 1)
|
if (buttonIndex == [alert firstOtherButtonIndex] + 1)
|
||||||
[manager useiCloudStore:YES alertUser:NO];
|
[manager useiCloudStore:YES alertUser:NO];
|
||||||
} cancelTitle:@"Leave iCloud Off" otherTitles:@"Explain?", @"Enable iCloud", nil];
|
} cancelTitle:@"Leave iCloud Off" otherTitles:@"Explain?", @"Enable iCloud", nil];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,14 +603,14 @@
|
|||||||
static NSDictionary *testFlightInfo = nil;
|
static NSDictionary *testFlightInfo = nil;
|
||||||
if (testFlightInfo == nil)
|
if (testFlightInfo == nil)
|
||||||
testFlightInfo = [[NSDictionary alloc] initWithContentsOfURL:
|
testFlightInfo = [[NSDictionary alloc] initWithContentsOfURL:
|
||||||
[[NSBundle mainBundle] URLForResource:@"TestFlight" withExtension:@"plist"]];
|
[[NSBundle mainBundle] URLForResource:@"TestFlight" withExtension:@"plist"]];
|
||||||
|
|
||||||
return testFlightInfo;
|
return testFlightInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSString *)testFlightToken {
|
- (NSString *)testFlightToken {
|
||||||
|
|
||||||
return NullToNil([[self testFlightInfo] valueForKeyPath:@"Team Token"]);
|
return NSNullToNil([[self testFlightInfo] valueForKeyPath:@"Team Token"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -491,14 +622,14 @@
|
|||||||
static NSDictionary *crashlyticsInfo = nil;
|
static NSDictionary *crashlyticsInfo = nil;
|
||||||
if (crashlyticsInfo == nil)
|
if (crashlyticsInfo == nil)
|
||||||
crashlyticsInfo = [[NSDictionary alloc] initWithContentsOfURL:
|
crashlyticsInfo = [[NSDictionary alloc] initWithContentsOfURL:
|
||||||
[[NSBundle mainBundle] URLForResource:@"Crashlytics" withExtension:@"plist"]];
|
[[NSBundle mainBundle] URLForResource:@"Crashlytics" withExtension:@"plist"]];
|
||||||
|
|
||||||
return crashlyticsInfo;
|
return crashlyticsInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSString *)crashlyticsAPIKey {
|
- (NSString *)crashlyticsAPIKey {
|
||||||
|
|
||||||
return NullToNil([[self crashlyticsInfo] valueForKeyPath:@"API Key"]);
|
return NSNullToNil([[self crashlyticsInfo] valueForKeyPath:@"API Key"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -510,7 +641,7 @@
|
|||||||
static NSDictionary *localyticsInfo = nil;
|
static NSDictionary *localyticsInfo = nil;
|
||||||
if (localyticsInfo == nil)
|
if (localyticsInfo == nil)
|
||||||
localyticsInfo = [[NSDictionary alloc] initWithContentsOfURL:
|
localyticsInfo = [[NSDictionary alloc] initWithContentsOfURL:
|
||||||
[[NSBundle mainBundle] URLForResource:@"Localytics" withExtension:@"plist"]];
|
[[NSBundle mainBundle] URLForResource:@"Localytics" withExtension:@"plist"]];
|
||||||
|
|
||||||
return localyticsInfo;
|
return localyticsInfo;
|
||||||
}
|
}
|
||||||
@@ -518,11 +649,9 @@
|
|||||||
- (NSString *)localyticsKey {
|
- (NSString *)localyticsKey {
|
||||||
|
|
||||||
#ifdef DEBUG
|
#ifdef DEBUG
|
||||||
return NullToNil([[self localyticsInfo] valueForKeyPath:@"Key.development"]);
|
return NSNullToNil([[self localyticsInfo] valueForKeyPath:@"Key.development"]);
|
||||||
#elif defined(LITE)
|
|
||||||
return NullToNil([[self localyticsInfo] valueForKeyPath:@"Key.distribution.lite"]);
|
|
||||||
#else
|
#else
|
||||||
return NullToNil([[self localyticsInfo] valueForKeyPath:@"Key.distribution"]);
|
return NSNullToNil([[self localyticsInfo] valueForKeyPath:@"Key.distribution"]);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,10 @@
|
|||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
@interface MPGuideViewController : UIViewController
|
@interface MPGuideViewController : UIViewController <UIScrollViewDelegate>
|
||||||
|
|
||||||
@property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
|
@property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIPageControl *pageControl;
|
||||||
|
|
||||||
- (IBAction)close;
|
- (IBAction)close;
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "MPGuideViewController.h"
|
#import "MPGuideViewController.h"
|
||||||
#import "MPAppDelegate.h"
|
|
||||||
|
|
||||||
|
|
||||||
@implementation MPGuideViewController
|
@implementation MPGuideViewController
|
||||||
@synthesize scrollView;
|
@synthesize scrollView;
|
||||||
|
@synthesize pageControl;
|
||||||
|
|
||||||
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
|
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
|
||||||
|
|
||||||
@@ -22,27 +22,34 @@
|
|||||||
|
|
||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
|
|
||||||
[PearlUIUtils autoSizeContent:self.scrollView];
|
[self.scrollView autoSizeContent];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewWillAppear:(BOOL)animated {
|
||||||
|
|
||||||
|
inf(@"Guide will appear.");
|
||||||
|
[super viewWillAppear:animated];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewDidAppear:(BOOL)animated {
|
||||||
|
|
||||||
|
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:animated? UIStatusBarAnimationSlide: UIStatusBarAnimationNone];
|
||||||
|
|
||||||
|
[super viewDidAppear:animated];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewWillDisappear:(BOOL)animated {
|
- (void)viewWillDisappear:(BOOL)animated {
|
||||||
|
|
||||||
|
inf(@"Guide will disappear.");
|
||||||
[super viewWillDisappear:animated];
|
[super viewWillDisappear:animated];
|
||||||
|
|
||||||
[MPiOSConfig get].showQuickStart = [NSNumber numberWithBool:NO];
|
[MPiOSConfig get].showQuickStart = [NSNumber numberWithBool:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewDidDisappear:(BOOL)animated {
|
|
||||||
|
|
||||||
[super viewDidDisappear:animated];
|
|
||||||
|
|
||||||
[[MPAppDelegate get] loadKey:animated];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
- (void)viewDidUnload {
|
- (void)viewDidUnload {
|
||||||
|
|
||||||
[self setScrollView:nil];
|
[self setScrollView:nil];
|
||||||
|
[self setPageControl:nil];
|
||||||
[super viewDidUnload];
|
[super viewDidUnload];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,4 +58,12 @@
|
|||||||
[self.presentingViewController dismissModalViewControllerAnimated:YES];
|
[self.presentingViewController dismissModalViewControllerAnimated:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView_ {
|
||||||
|
|
||||||
|
NSInteger page = (NSInteger)(self.scrollView.contentOffset.x / self.scrollView.bounds.size.width);
|
||||||
|
|
||||||
|
self.pageControl.currentPage = page;
|
||||||
|
self.pageControl.hidden = (page == self.pageControl.numberOfPages - 1);
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -6,42 +6,64 @@
|
|||||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
#import <MessageUI/MessageUI.h>
|
||||||
#import "MPTypeViewController.h"
|
#import "MPTypeViewController.h"
|
||||||
#import "MPElementEntity.h"
|
#import "MPElementEntity.h"
|
||||||
#import "MPSearchDelegate.h"
|
#import "MPSearchDelegate.h"
|
||||||
#import "IASKAppSettingsViewController.h"
|
|
||||||
|
|
||||||
@interface MPMainViewController : UIViewController <MPTypeDelegate, UITextFieldDelegate, MPSearchResultsDelegate, UIWebViewDelegate, IASKSettingsDelegate>
|
@interface MPMainViewController : UIViewController<MPTypeDelegate, UITextFieldDelegate, MPSearchResultsDelegate, UIWebViewDelegate, MFMailComposeViewControllerDelegate, UIGestureRecognizerDelegate>
|
||||||
|
|
||||||
|
@property (nonatomic, assign) BOOL userNameHidden;
|
||||||
|
@property (strong, nonatomic) MPElementEntity *activeElement;
|
||||||
|
@property (strong, nonatomic) IBOutlet MPSearchDelegate *searchDelegate;
|
||||||
|
@property (weak, nonatomic) IBOutlet UITextField *contentField;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIButton *typeButton;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIWebView *helpView;
|
||||||
|
@property (weak, nonatomic) IBOutlet UILabel *siteName;
|
||||||
|
@property (weak, nonatomic) IBOutlet UILabel *passwordCounter;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIButton *passwordIncrementer;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIButton *passwordEdit;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIButton *passwordUpgrade;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *contentContainer;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *displayContainer;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *helpContainer;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *contentTipContainer;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *userNameTipContainer;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *alertContainer;
|
||||||
|
@property (weak, nonatomic) IBOutlet UILabel *alertTitle;
|
||||||
|
@property (weak, nonatomic) IBOutlet UITextView *alertBody;
|
||||||
|
@property (weak, nonatomic) IBOutlet UILabel *contentTipBody;
|
||||||
|
@property (weak, nonatomic) IBOutlet UILabel *userNameTipBody;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIImageView *toolTipEditIcon;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *searchTipContainer;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *actionsTipContainer;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *typeTipContainer;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *toolTipContainer;
|
||||||
|
@property (weak, nonatomic) IBOutlet UILabel *toolTipBody;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *userNameContainer;
|
||||||
|
@property (weak, nonatomic) IBOutlet UITextField *userNameField;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIButton *passwordUser;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *outdatedAlertContainer;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIImageView *outdatedAlertBack;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIButton *outdatedAlertCloseButton;
|
||||||
|
|
||||||
@property (strong, nonatomic) MPElementEntity *activeElement;
|
|
||||||
@property (strong, nonatomic) IBOutlet MPSearchDelegate *searchResultsController;
|
|
||||||
@property (weak, nonatomic) IBOutlet UITextField *contentField;
|
|
||||||
@property (weak, nonatomic) IBOutlet UIButton *typeButton;
|
|
||||||
@property (weak, nonatomic) IBOutlet UIWebView *helpView;
|
|
||||||
@property (weak, nonatomic) IBOutlet UILabel *siteName;
|
|
||||||
@property (weak, nonatomic) IBOutlet UILabel *passwordCounter;
|
|
||||||
@property (weak, nonatomic) IBOutlet UIButton *passwordIncrementer;
|
|
||||||
@property (weak, nonatomic) IBOutlet UIButton *passwordEdit;
|
|
||||||
@property (weak, nonatomic) IBOutlet UIView *contentContainer;
|
|
||||||
@property (weak, nonatomic) IBOutlet UIView *helpContainer;
|
|
||||||
@property (weak, nonatomic) IBOutlet UIView *contentTipContainer;
|
|
||||||
@property (weak, nonatomic) IBOutlet UIView *alertContainer;
|
|
||||||
@property (weak, nonatomic) IBOutlet UILabel *alertTitle;
|
|
||||||
@property (weak, nonatomic) IBOutlet UITextView *alertBody;
|
|
||||||
@property (weak, nonatomic) IBOutlet UILabel *contentTipBody;
|
|
||||||
@property (weak, nonatomic) IBOutlet UIImageView *contentTipEditIcon;
|
|
||||||
@property (weak, nonatomic) IBOutlet UIView *searchTipContainer;
|
|
||||||
|
|
||||||
@property (copy) void (^contentTipCleanup)(BOOL finished);
|
@property (copy) void (^contentTipCleanup)(BOOL finished);
|
||||||
|
@property (copy) void (^toolTipCleanup)(BOOL finished);
|
||||||
|
|
||||||
- (IBAction)copyContent;
|
- (IBAction)copyContent;
|
||||||
- (IBAction)incrementPasswordCounter;
|
- (IBAction)incrementPasswordCounter;
|
||||||
- (IBAction)resetPasswordCounter:(UILongPressGestureRecognizer *)sender;
|
- (IBAction)resetPasswordCounter:(UILongPressGestureRecognizer *)sender;
|
||||||
|
- (IBAction)editUserName:(UILongPressGestureRecognizer *)sender;
|
||||||
- (IBAction)editPassword;
|
- (IBAction)editPassword;
|
||||||
- (IBAction)closeAlert;
|
- (IBAction)closeAlert;
|
||||||
|
- (IBAction)upgradePassword;
|
||||||
- (IBAction)action:(UIBarButtonItem *)sender;
|
- (IBAction)action:(UIBarButtonItem *)sender;
|
||||||
|
- (IBAction)toggleUser;
|
||||||
|
- (IBAction)searchOutdatedElements;
|
||||||
|
- (IBAction)closeOutdatedAlert;
|
||||||
|
- (IBAction)infoOutdatedAlert;
|
||||||
|
|
||||||
- (BOOL)isHelpVisible;
|
|
||||||
- (void)toggleHelpAnimated:(BOOL)animated;
|
- (void)toggleHelpAnimated:(BOOL)animated;
|
||||||
- (void)setHelpHidden:(BOOL)hidden animated:(BOOL)animated;
|
- (void)setHelpHidden:(BOOL)hidden animated:(BOOL)animated;
|
||||||
- (void)setHelpChapter:(NSString *)chapter;
|
- (void)setHelpChapter:(NSString *)chapter;
|
||||||
|
|||||||
25
MasterPassword/iOS/MPPreferencesViewController.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// MPPreferencesViewController.h
|
||||||
|
// MasterPassword-iOS
|
||||||
|
//
|
||||||
|
// Created by Maarten Billemont on 04/06/12.
|
||||||
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
#import "IASKAppSettingsViewController.h"
|
||||||
|
#import "MPTypeViewController.h"
|
||||||
|
|
||||||
|
@interface MPPreferencesViewController : UITableViewController<IASKSettingsDelegate, MPTypeDelegate>
|
||||||
|
|
||||||
|
@property (weak, nonatomic) IBOutlet UIScrollView *avatarsView;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIButton *avatarTemplate;
|
||||||
|
@property (weak, nonatomic) IBOutlet UISwitch *savePasswordSwitch;
|
||||||
|
@property (weak, nonatomic) IBOutlet UITableViewCell *exportCell;
|
||||||
|
@property (weak, nonatomic) IBOutlet UITableViewCell *changeMPCell;
|
||||||
|
@property (weak, nonatomic) IBOutlet UILabel *defaultTypeLabel;
|
||||||
|
|
||||||
|
- (IBAction)didToggleSwitch:(UISwitch *)sender;
|
||||||
|
- (IBAction)settings:(id)sender;
|
||||||
|
|
||||||
|
@end
|
||||||
169
MasterPassword/iOS/MPPreferencesViewController.m
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
//
|
||||||
|
// MPPreferencesViewController.m
|
||||||
|
// MasterPassword-iOS
|
||||||
|
//
|
||||||
|
// Created by Maarten Billemont on 04/06/12.
|
||||||
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <QuartzCore/QuartzCore.h>
|
||||||
|
#import "MPPreferencesViewController.h"
|
||||||
|
#import "MPAppDelegate.h"
|
||||||
|
#import "MPAppDelegate_Key.h"
|
||||||
|
#import "MPAppDelegate_Store.h"
|
||||||
|
|
||||||
|
@interface MPPreferencesViewController ()
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation MPPreferencesViewController
|
||||||
|
@synthesize avatarsView;
|
||||||
|
@synthesize avatarTemplate;
|
||||||
|
@synthesize savePasswordSwitch;
|
||||||
|
@synthesize exportCell;
|
||||||
|
@synthesize changeMPCell;
|
||||||
|
@synthesize defaultTypeLabel;
|
||||||
|
|
||||||
|
|
||||||
|
- (void)viewDidLoad {
|
||||||
|
|
||||||
|
self.avatarTemplate.hidden = YES;
|
||||||
|
|
||||||
|
for (int a = 0; a < MPAvatarCount; ++a) {
|
||||||
|
UIButton *avatar = [self.avatarTemplate clone];
|
||||||
|
avatar.tag = a;
|
||||||
|
avatar.hidden = NO;
|
||||||
|
avatar.center = CGPointMake(
|
||||||
|
self.avatarTemplate.center.x * (a + 1) + self.avatarTemplate.bounds.size.width / 2 * a,
|
||||||
|
self.avatarTemplate.center.y);
|
||||||
|
[avatar setBackgroundImage:[UIImage imageNamed:PearlString(@"avatar-%d", a)]
|
||||||
|
forState:UIControlStateNormal];
|
||||||
|
[avatar setSelectionInSuperviewCandidate:YES isClearable:NO];
|
||||||
|
|
||||||
|
avatar.layer.cornerRadius = avatar.bounds.size.height / 2;
|
||||||
|
avatar.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||||
|
avatar.layer.shadowOpacity = 1;
|
||||||
|
avatar.layer.shadowRadius = 5;
|
||||||
|
avatar.backgroundColor = [UIColor clearColor];
|
||||||
|
|
||||||
|
[avatar onHighlightOrSelect:^(BOOL highlighted, BOOL selected) {
|
||||||
|
if (highlighted || selected)
|
||||||
|
avatar.backgroundColor = self.avatarTemplate.backgroundColor;
|
||||||
|
else
|
||||||
|
avatar.backgroundColor = [UIColor clearColor];
|
||||||
|
} options:0];
|
||||||
|
[avatar onSelect:^(BOOL selected) {
|
||||||
|
if (selected) {
|
||||||
|
[MPAppDelegate get].activeUser.avatar = (unsigned)avatar.tag;
|
||||||
|
[[MPAppDelegate get] saveContext];
|
||||||
|
}
|
||||||
|
} options:0];
|
||||||
|
avatar.selected = (a == [MPAppDelegate get].activeUser.avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
[super viewDidLoad];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewWillAppear:(BOOL)animated {
|
||||||
|
|
||||||
|
inf(@"Preferences will appear");
|
||||||
|
[self.avatarsView autoSizeContent];
|
||||||
|
[self.avatarsView enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
|
||||||
|
if (subview.tag && ((UIControl *)subview).selected) {
|
||||||
|
[self.avatarsView setContentOffset:CGPointMake(subview.center.x - self.avatarsView.bounds.size.width / 2, 0) animated:animated];
|
||||||
|
}
|
||||||
|
} recurse:NO];
|
||||||
|
|
||||||
|
self.savePasswordSwitch.on = [MPAppDelegate get].activeUser.saveKey;
|
||||||
|
self.defaultTypeLabel.text = [[MPAppDelegate get].key.algorithm shortNameOfType:[MPAppDelegate get].activeUser.defaultType];
|
||||||
|
|
||||||
|
[super viewWillAppear:animated];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewWillDisappear:(BOOL)animated {
|
||||||
|
|
||||||
|
inf(@"Preferences will disappear");
|
||||||
|
[super viewWillDisappear:animated];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
|
||||||
|
|
||||||
|
return (interfaceOrientation == UIInterfaceOrientationPortrait);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewDidUnload {
|
||||||
|
|
||||||
|
[self setAvatarsView:nil];
|
||||||
|
[self setAvatarTemplate:nil];
|
||||||
|
[self setAvatarsView:nil];
|
||||||
|
[self setSavePasswordSwitch:nil];
|
||||||
|
[self setExportCell:nil];
|
||||||
|
[self setChangeMPCell:nil];
|
||||||
|
[self setDefaultTypeLabel:nil];
|
||||||
|
[super viewDidUnload];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
|
||||||
|
|
||||||
|
if ([[segue identifier] isEqualToString:@"MP_ChooseType"])
|
||||||
|
((MPTypeViewController *)[segue destinationViewController]).delegate = self;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - UITableViewDelegate
|
||||||
|
|
||||||
|
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
|
|
||||||
|
UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];
|
||||||
|
if (cell == self.exportCell)
|
||||||
|
[[MPAppDelegate get] export];
|
||||||
|
|
||||||
|
else
|
||||||
|
if (cell == self.changeMPCell)
|
||||||
|
[[MPAppDelegate get] changeMasterPasswordFor:[MPAppDelegate get].activeUser didResetBlock:nil];
|
||||||
|
|
||||||
|
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - IASKSettingsDelegate
|
||||||
|
|
||||||
|
- (void)settingsViewControllerDidEnd:(IASKAppSettingsViewController *)sender {
|
||||||
|
|
||||||
|
while ([self.navigationController.viewControllers containsObject:sender])
|
||||||
|
[self.navigationController popViewControllerAnimated:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - MPTypeDelegate
|
||||||
|
|
||||||
|
- (void)didSelectType:(MPElementType)type {
|
||||||
|
|
||||||
|
[MPAppDelegate get].activeUser.defaultType = type;
|
||||||
|
[[MPAppDelegate get] saveContext];
|
||||||
|
|
||||||
|
self.defaultTypeLabel.text = [[MPAppDelegate get].key.algorithm shortNameOfType:[MPAppDelegate get].activeUser.defaultType];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MPElementType)selectedType {
|
||||||
|
|
||||||
|
return [MPAppDelegate get].activeUser.defaultType;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - IBActions
|
||||||
|
|
||||||
|
- (IBAction)didToggleSwitch:(UISwitch *)sender {
|
||||||
|
|
||||||
|
if (([MPAppDelegate get].activeUser.saveKey = sender.on))
|
||||||
|
[[MPAppDelegate get] storeSavedKeyFor:[MPAppDelegate get].activeUser];
|
||||||
|
else
|
||||||
|
[[MPAppDelegate get] forgetSavedKeyFor:[MPAppDelegate get].activeUser];
|
||||||
|
[[MPAppDelegate get] saveContext];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (IBAction)settings:(UIBarButtonItem *)sender {
|
||||||
|
|
||||||
|
IASKAppSettingsViewController *vc = [IASKAppSettingsViewController new];
|
||||||
|
vc.showDoneButton = NO;
|
||||||
|
|
||||||
|
[self.navigationController pushViewController:vc animated:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -9,21 +9,26 @@
|
|||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#import "MPElementEntity.h"
|
#import "MPElementEntity.h"
|
||||||
|
|
||||||
@protocol MPSearchResultsDelegate <NSObject>
|
typedef enum {
|
||||||
|
MPSearchScopeAll,
|
||||||
|
MPSearchScopeOutdated,
|
||||||
|
} MPSearchScope;
|
||||||
|
|
||||||
|
@protocol MPSearchResultsDelegate<NSObject>
|
||||||
|
|
||||||
- (void)didSelectElement:(MPElementEntity *)element;
|
- (void)didSelectElement:(MPElementEntity *)element;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface MPSearchDelegate : NSObject <UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UISearchDisplayDelegate, NSFetchedResultsControllerDelegate>
|
@interface MPSearchDelegate : NSObject<UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, UISearchDisplayDelegate, NSFetchedResultsControllerDelegate>
|
||||||
|
|
||||||
@property (strong, nonatomic) NSDateFormatter *dateFormatter;
|
@property (strong, nonatomic) NSDateFormatter *dateFormatter;
|
||||||
@property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController;
|
@property (strong, readonly) NSFetchedResultsController *fetchedResultsController;
|
||||||
@property (strong, nonatomic) NSString *query;
|
@property (strong, nonatomic) NSString *query;
|
||||||
@property (strong, nonatomic) UILabel *tipView;
|
@property (strong, nonatomic) UILabel *tipView;
|
||||||
|
|
||||||
@property (weak, nonatomic) IBOutlet id<MPSearchResultsDelegate> delegate;
|
@property (weak, nonatomic) IBOutlet id<MPSearchResultsDelegate> delegate;
|
||||||
@property (weak, nonatomic) IBOutlet UISearchDisplayController *searchDisplayController;
|
@property (strong, nonatomic) IBOutlet UISearchDisplayController *searchDisplayController;
|
||||||
@property (weak, nonatomic) IBOutlet UIView *searchTipContainer;
|
@property (weak, nonatomic) IBOutlet UIView *searchTipContainer;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -8,21 +8,22 @@
|
|||||||
|
|
||||||
#import "MPSearchDelegate.h"
|
#import "MPSearchDelegate.h"
|
||||||
#import "MPAppDelegate.h"
|
#import "MPAppDelegate.h"
|
||||||
|
#import "LocalyticsSession.h"
|
||||||
#import "MPAppDelegate_Store.h"
|
#import "MPAppDelegate_Store.h"
|
||||||
#import "MPElementGeneratedEntity.h"
|
|
||||||
|
|
||||||
@interface MPSearchDelegate (Private)
|
@interface MPSearchDelegate (Private)
|
||||||
|
|
||||||
- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath;
|
- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath;
|
||||||
- (void)update;
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation MPSearchDelegate
|
@implementation MPSearchDelegate {
|
||||||
|
|
||||||
|
NSFetchedResultsController *_fetchedResultsController;
|
||||||
|
}
|
||||||
@synthesize tipView;
|
@synthesize tipView;
|
||||||
@synthesize query;
|
@synthesize query;
|
||||||
@synthesize dateFormatter;
|
@synthesize dateFormatter;
|
||||||
@synthesize fetchedResultsController;
|
|
||||||
@synthesize delegate;
|
@synthesize delegate;
|
||||||
@synthesize searchDisplayController;
|
@synthesize searchDisplayController;
|
||||||
@synthesize searchTipContainer;
|
@synthesize searchTipContainer;
|
||||||
@@ -32,36 +33,47 @@
|
|||||||
if (!([super init]))
|
if (!([super init]))
|
||||||
return nil;
|
return nil;
|
||||||
|
|
||||||
self.dateFormatter = [NSDateFormatter new];
|
self.dateFormatter = [NSDateFormatter new];
|
||||||
self.dateFormatter.dateStyle = NSDateFormatterShortStyle;
|
self.dateFormatter.dateStyle = NSDateFormatterShortStyle;
|
||||||
self.query = @"";
|
self.query = @"";
|
||||||
|
|
||||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
self.tipView = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 320, 170)];
|
||||||
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]];
|
self.tipView.textAlignment = UITextAlignmentCenter;
|
||||||
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
|
self.tipView.backgroundColor = [UIColor clearColor];
|
||||||
managedObjectContext:[MPAppDelegate managedObjectContext]
|
self.tipView.textColor = [UIColor lightTextColor];
|
||||||
sectionNameKeyPath:nil cacheName:nil];
|
self.tipView.shadowColor = [UIColor blackColor];
|
||||||
self.fetchedResultsController.delegate = self;
|
self.tipView.shadowOffset = CGSizeMake(0, -1);
|
||||||
|
self.tipView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight
|
||||||
self.tipView = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 320, 170)];
|
| UIViewAutoresizingFlexibleBottomMargin;
|
||||||
self.tipView.textAlignment = UITextAlignmentCenter;
|
self.tipView.numberOfLines = 0;
|
||||||
self.tipView.backgroundColor = [UIColor clearColor];
|
self.tipView.font = [UIFont systemFontOfSize:14];
|
||||||
self.tipView.textColor = [UIColor lightTextColor];
|
|
||||||
self.tipView.shadowColor = [UIColor blackColor];
|
|
||||||
self.tipView.shadowOffset = CGSizeMake(0, -1);
|
|
||||||
self.tipView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleBottomMargin;
|
|
||||||
self.tipView.numberOfLines = 0;
|
|
||||||
self.tipView.font = [UIFont systemFontOfSize:14];
|
|
||||||
self.tipView.text =
|
self.tipView.text =
|
||||||
@"Tip:\n"
|
@"Tip:\n"
|
||||||
@"Name your sites by their domain name:\n"
|
@"Name your sites by their domain name:\n"
|
||||||
@"apple.com, twitter.com\n\n"
|
@"apple.com, twitter.com\n\n"
|
||||||
@"For email accounts, use the address:\n"
|
@"For email accounts, use the address:\n"
|
||||||
@"john@apple.com, john@gmail.com";
|
@"john@apple.com, john@gmail.com";
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSFetchedResultsController *)fetchedResultsController {
|
||||||
|
|
||||||
|
if (!_fetchedResultsController) {
|
||||||
|
NSManagedObjectContext *moc = [MPAppDelegate managedObjectContextIfReady];
|
||||||
|
if (!moc)
|
||||||
|
return nil;
|
||||||
|
|
||||||
|
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
||||||
|
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses_" ascending:NO]];
|
||||||
|
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:moc
|
||||||
|
sectionNameKeyPath:nil cacheName:nil];
|
||||||
|
_fetchedResultsController.delegate = self;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _fetchedResultsController;
|
||||||
|
}
|
||||||
|
|
||||||
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
|
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
|
||||||
|
|
||||||
UITableView *tableView = self.searchDisplayController.searchResultsTableView;
|
UITableView *tableView = self.searchDisplayController.searchResultsTableView;
|
||||||
@@ -78,7 +90,7 @@
|
|||||||
|
|
||||||
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
|
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
|
||||||
|
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointCancelSearch];
|
[TestFlight passCheckpoint:MPCheckpointCancelSearch];
|
||||||
|
|
||||||
[self.delegate didSelectElement:nil];
|
[self.delegate didSelectElement:nil];
|
||||||
}
|
}
|
||||||
@@ -88,14 +100,19 @@
|
|||||||
if (searchBar.searchResultsButtonSelected && !searchText.length)
|
if (searchBar.searchResultsButtonSelected && !searchText.length)
|
||||||
searchBar.text = @" ";
|
searchBar.text = @" ";
|
||||||
|
|
||||||
self.query = [searchBar.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
self.query = [searchBar.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||||
if (!self.query)
|
if (!self.query)
|
||||||
self.query = @"";
|
self.query = @"";
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller {
|
- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller {
|
||||||
|
|
||||||
controller.searchBar.prompt = @"Enter the site's name:";
|
controller.searchBar.prompt = @"Enter the site's name:";
|
||||||
|
controller.searchBar.showsScopeBar = controller.searchBar.selectedScopeButtonIndex != MPSearchScopeAll;
|
||||||
|
if (controller.searchBar.showsScopeBar)
|
||||||
|
controller.searchBar.scopeButtonTitles = [NSArray arrayWithObjects:@"All", @"Outdated", nil];
|
||||||
|
else
|
||||||
|
controller.searchBar.scopeButtonTitles = nil;
|
||||||
|
|
||||||
[UIView animateWithDuration:0.2f animations:^{
|
[UIView animateWithDuration:0.2f animations:^{
|
||||||
self.searchTipContainer.alpha = 0;
|
self.searchTipContainer.alpha = 0;
|
||||||
@@ -105,50 +122,82 @@
|
|||||||
- (void)searchDisplayControllerDidBeginSearch:(UISearchDisplayController *)controller {
|
- (void)searchDisplayControllerDidBeginSearch:(UISearchDisplayController *)controller {
|
||||||
|
|
||||||
controller.searchBar.text = controller.searchBar.searchResultsButtonSelected? @" ": @"";
|
controller.searchBar.text = controller.searchBar.searchResultsButtonSelected? @" ": @"";
|
||||||
self.query = @"";
|
self.query = @"";
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller {
|
- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller {
|
||||||
|
|
||||||
controller.searchBar.prompt = nil;
|
controller.searchBar.prompt = nil;
|
||||||
controller.searchBar.searchResultsButtonSelected = NO;
|
controller.searchBar.searchResultsButtonSelected = NO;
|
||||||
|
controller.searchBar.selectedScopeButtonIndex = MPSearchScopeAll;
|
||||||
|
controller.searchBar.showsScopeBar = NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)searchDisplayController:(UISearchDisplayController *)controller didLoadSearchResultsTableView:(UITableView *)tableView {
|
- (void)searchDisplayController:(UISearchDisplayController *)controller didLoadSearchResultsTableView:(UITableView *)tableView {
|
||||||
|
|
||||||
tableView.backgroundColor = [UIColor blackColor];
|
tableView.backgroundColor = [UIColor blackColor];
|
||||||
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||||
tableView.rowHeight = 48.0f;
|
tableView.rowHeight = 48.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString {
|
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString {
|
||||||
|
|
||||||
[self update];
|
if (!controller.active)
|
||||||
|
return NO;
|
||||||
|
|
||||||
return NO;
|
[self fetchData];
|
||||||
|
|
||||||
|
return YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)update {
|
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption {
|
||||||
|
|
||||||
|
if (!controller.active)
|
||||||
|
return NO;
|
||||||
|
|
||||||
|
[self fetchData];
|
||||||
|
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)fetchData {
|
||||||
|
|
||||||
assert(self.query);
|
assert(self.query);
|
||||||
|
assert([MPAppDelegate get].activeUser);
|
||||||
|
|
||||||
self.fetchedResultsController.fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND keyID == %@",
|
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"user == %@", [MPAppDelegate get].activeUser];
|
||||||
self.query, self.query, NilToNull([MPAppDelegate get].keyID)];
|
if (self.query.length)
|
||||||
|
predicate = [NSCompoundPredicate
|
||||||
|
andPredicateWithSubpredicates:[NSArray arrayWithObjects:[NSPredicate predicateWithFormat:@"name BEGINSWITH[cd] %@", self.query],
|
||||||
|
predicate, nil]];
|
||||||
|
|
||||||
|
switch ((MPSearchScope)self.searchDisplayController.searchBar.selectedScopeButtonIndex) {
|
||||||
|
|
||||||
|
case MPSearchScopeAll:
|
||||||
|
break;
|
||||||
|
case MPSearchScopeOutdated:
|
||||||
|
predicate = [NSCompoundPredicate
|
||||||
|
andPredicateWithSubpredicates:[NSArray arrayWithObjects:[NSPredicate predicateWithFormat:@"requiresExplicitMigration_ == YES"],
|
||||||
|
predicate, nil]];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
self.fetchedResultsController.fetchRequest.predicate = predicate;
|
||||||
|
|
||||||
NSError *error;
|
NSError *error;
|
||||||
if (![self.fetchedResultsController performFetch:&error])
|
if (![self.fetchedResultsController performFetch:&error])
|
||||||
err(@"Couldn't fetch elements: %@", error);
|
err(@"Couldn't fetch elements: %@", error);
|
||||||
[self.searchDisplayController.searchResultsTableView reloadData];
|
|
||||||
|
|
||||||
NSArray *subviews = self.searchDisplayController.searchBar.superview.subviews;
|
[self.searchDisplayController.searchBar.superview enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
|
||||||
NSUInteger overlayIndex = [subviews indexOfObject:self.searchDisplayController.searchBar] + 1;
|
CGRect searchBarFrame = self.searchDisplayController.searchBar.frame;
|
||||||
UIView *overlay = [subviews count] > overlayIndex? [subviews objectAtIndex:overlayIndex]: nil;
|
if ([subview isKindOfClass:[UIControl class]] &&
|
||||||
if (overlay == self.searchDisplayController.searchResultsTableView || ![overlay isKindOfClass:[UIControl class]])
|
CGPointEqualToPoint(
|
||||||
overlay = nil;
|
CGPointDistanceBetweenCGPoints(searchBarFrame.origin, subview.frame.origin),
|
||||||
if (self.tipView.superview != overlay) {
|
CGPointMake(0, searchBarFrame.size.height))) {
|
||||||
[self.tipView removeFromSuperview];
|
[self.tipView removeFromSuperview];
|
||||||
[overlay addSubview:self.tipView];
|
[subview addSubview:self.tipView];
|
||||||
}
|
*stop = YES;
|
||||||
|
}
|
||||||
|
} recurse:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
// See MP-14, also crashes easily on internal assertions etc..
|
// See MP-14, also crashes easily on internal assertions etc..
|
||||||
@@ -206,6 +255,7 @@
|
|||||||
|
|
||||||
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
|
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
|
||||||
|
|
||||||
|
dbg(@"controllerDidChangeContent on thread: %@", [NSThread currentThread].name);
|
||||||
[self.searchDisplayController.searchResultsTableView reloadData];
|
[self.searchDisplayController.searchResultsTableView reloadData];
|
||||||
// [self.searchDisplayController.searchResultsTableView endUpdates];
|
// [self.searchDisplayController.searchResultsTableView endUpdates];
|
||||||
}
|
}
|
||||||
@@ -219,28 +269,28 @@
|
|||||||
__block BOOL hasExactQueryMatch = NO;
|
__block BOOL hasExactQueryMatch = NO;
|
||||||
[sections enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
|
[sections enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
|
||||||
id<NSFetchedResultsSectionInfo> sectionInfo = obj;
|
id<NSFetchedResultsSectionInfo> sectionInfo = obj;
|
||||||
[[sectionInfo objects] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
|
[[sectionInfo objects] enumerateObjectsUsingBlock:^(id obj_, NSUInteger idx_, BOOL *stop_) {
|
||||||
if ([[obj name] isEqualToString:self.query]) {
|
if ([[obj_ name] isEqualToString:self.query]) {
|
||||||
hasExactQueryMatch = YES;
|
hasExactQueryMatch = YES;
|
||||||
*stop = YES;
|
*stop_ = YES;
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
if (hasExactQueryMatch)
|
if (hasExactQueryMatch)
|
||||||
*stop = YES;
|
*stop = YES;
|
||||||
}];
|
}];
|
||||||
if (!hasExactQueryMatch)
|
if (!hasExactQueryMatch)
|
||||||
// Add a section for "new site".
|
// Add a section for "new site".
|
||||||
++sectionCount;
|
++sectionCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (signed)sectionCount;
|
return (NSInteger)sectionCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||||
|
|
||||||
NSArray *sections = [self.fetchedResultsController sections];
|
NSArray *sections = [self.fetchedResultsController sections];
|
||||||
if (section < (signed)[sections count])
|
if (section < (NSInteger)[sections count])
|
||||||
return (signed)[[sections objectAtIndex:(unsigned)section] numberOfObjects];
|
return (NSInteger)[[sections objectAtIndex:(unsigned)section] numberOfObjects];
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -252,20 +302,20 @@
|
|||||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"MPElementSearch"];
|
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"MPElementSearch"];
|
||||||
|
|
||||||
UIImageView *backgroundImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ui_list_middle"]];
|
UIImageView *backgroundImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ui_list_middle"]];
|
||||||
backgroundImageView.frame = CGRectMake(-5, 0, 330, 34);
|
backgroundImageView.frame = CGRectMake(-5, 0, 330, 34);
|
||||||
backgroundImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
backgroundImageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||||
backgroundImageView.contentStretch = CGRectMake(0.2f, 0.2f, 0.6f, 0.6f);
|
backgroundImageView.contentStretch = CGRectMake(0.2f, 0.2f, 0.6f, 0.6f);
|
||||||
UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 34)];
|
UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 34)];
|
||||||
[backgroundView addSubview:backgroundImageView];
|
[backgroundView addSubview:backgroundImageView];
|
||||||
backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||||
|
|
||||||
cell.backgroundView = backgroundView;
|
cell.backgroundView = backgroundView;
|
||||||
cell.textLabel.backgroundColor = [UIColor clearColor];
|
cell.textLabel.backgroundColor = [UIColor clearColor];
|
||||||
cell.textLabel.textColor = [UIColor whiteColor];
|
cell.textLabel.textColor = [UIColor whiteColor];
|
||||||
cell.detailTextLabel.backgroundColor = [UIColor clearColor];
|
cell.detailTextLabel.backgroundColor = [UIColor clearColor];
|
||||||
cell.detailTextLabel.textColor = [UIColor lightGrayColor];
|
cell.detailTextLabel.textColor = [UIColor lightGrayColor];
|
||||||
cell.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
cell.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||||
cell.clipsToBounds = YES;
|
cell.clipsToBounds = YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
[self configureCell:cell inTableView:tableView atIndexPath:indexPath];
|
[self configureCell:cell inTableView:tableView atIndexPath:indexPath];
|
||||||
@@ -275,56 +325,58 @@
|
|||||||
|
|
||||||
- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath {
|
- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath {
|
||||||
|
|
||||||
if (indexPath.section < (signed)[[self.fetchedResultsController sections] count]) {
|
if (indexPath.section < (NSInteger)[[self.fetchedResultsController sections] count]) {
|
||||||
MPElementEntity *element = [self.fetchedResultsController objectAtIndexPath:indexPath];
|
MPElementEntity *element = [self.fetchedResultsController objectAtIndexPath:indexPath];
|
||||||
|
|
||||||
cell.textLabel.text = element.name;
|
cell.textLabel.text = element.name;
|
||||||
cell.detailTextLabel.text = [NSString stringWithFormat:@"Used %d times, last on %@",
|
cell.detailTextLabel.text = [NSString stringWithFormat:@"Used %d times, last on %@",
|
||||||
element.uses, [self.dateFormatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:element.lastUsed]]];
|
element.uses, [self.dateFormatter stringFromDate:element.lastUsed]];
|
||||||
} else {
|
} else {
|
||||||
// "New" section
|
// "New" section
|
||||||
cell.textLabel.text = self.query;
|
cell.textLabel.text = self.query;
|
||||||
cell.detailTextLabel.text = @"Create a new site.";
|
cell.detailTextLabel.text = @"Create a new site.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
|
|
||||||
if (indexPath.section < (signed)[[self.fetchedResultsController sections] count])
|
if (indexPath.section < (NSInteger)[[self.fetchedResultsController sections] count])
|
||||||
[self.delegate didSelectElement:[self.fetchedResultsController objectAtIndexPath:indexPath]];
|
[self.delegate didSelectElement:[self.fetchedResultsController objectAtIndexPath:indexPath]];
|
||||||
|
|
||||||
else {
|
else {
|
||||||
// "New" section.
|
// "New" section.
|
||||||
NSString *siteName = self.query;
|
NSString *siteName = self.query;
|
||||||
[PearlAlert showAlertWithTitle:@"New Site"
|
[PearlAlert showAlertWithTitle:@"New Site"
|
||||||
message:PearlLocalize(@"Do you want to create a new site named:\n%@", siteName)
|
message:PearlString(@"Do you want to create a new site named:\n%@", siteName)
|
||||||
viewStyle:UIAlertViewStyleDefault
|
viewStyle:UIAlertViewStyleDefault
|
||||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||||
|
|
||||||
if (buttonIndex == [alert cancelButtonIndex])
|
if (buttonIndex == [alert cancelButtonIndex])
|
||||||
return;
|
return;
|
||||||
|
|
||||||
[self.fetchedResultsController.managedObjectContext performBlock:^{
|
[self.fetchedResultsController.managedObjectContext performBlock:^{
|
||||||
MPElementGeneratedEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class])
|
MPElementType type = [MPAppDelegate get].activeUser.defaultType;
|
||||||
inManagedObjectContext:self.fetchedResultsController.managedObjectContext];
|
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:[MPAlgorithmDefault classNameOfType:type]
|
||||||
assert([element isKindOfClass:ClassFromMPElementType((unsigned)element.type)]);
|
inManagedObjectContext:self.fetchedResultsController.managedObjectContext];
|
||||||
assert([MPAppDelegate get].keyID);
|
assert([MPAppDelegate get].activeUser);
|
||||||
|
|
||||||
element.name = siteName;
|
element.name = siteName;
|
||||||
element.keyID = [MPAppDelegate get].keyID;
|
element.user = [MPAppDelegate get].activeUser;
|
||||||
|
element.type = type;
|
||||||
|
element.version = MPAlgorithmDefaultVersion;
|
||||||
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[self.delegate didSelectElement:element];
|
[self.delegate didSelectElement:element];
|
||||||
});
|
});
|
||||||
}];
|
}];
|
||||||
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonYes, nil];
|
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonYes, nil];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||||
|
|
||||||
if (section < (signed)[[self.fetchedResultsController sections] count])
|
if (section < (NSInteger)[[self.fetchedResultsController sections] count])
|
||||||
return [[[self.fetchedResultsController sections] objectAtIndex:(unsigned)section] name];
|
return [[[self.fetchedResultsController sections] objectAtIndex:(unsigned)section] name];
|
||||||
|
|
||||||
return @"";
|
return @"";
|
||||||
@@ -340,15 +392,23 @@
|
|||||||
return [self.fetchedResultsController sectionForSectionIndexTitle:title atIndex:index];
|
return [self.fetchedResultsController sectionForSectionIndexTitle:title atIndex:index];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
|
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
|
||||||
|
forRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||||
|
|
||||||
if (indexPath.section < (signed)[[self.fetchedResultsController sections] count]) {
|
if (indexPath.section < (NSInteger)[[self.fetchedResultsController sections] count]) {
|
||||||
if (editingStyle == UITableViewCellEditingStyleDelete)
|
if (editingStyle == UITableViewCellEditingStyleDelete)
|
||||||
[self.fetchedResultsController.managedObjectContext performBlock:^{
|
[self.fetchedResultsController.managedObjectContext performBlock:^{
|
||||||
MPElementEntity *element = [self.fetchedResultsController objectAtIndexPath:indexPath];
|
MPElementEntity *element = [self.fetchedResultsController objectAtIndexPath:indexPath];
|
||||||
|
|
||||||
|
inf(@"Deleting element: %@", element.name);
|
||||||
[self.fetchedResultsController.managedObjectContext deleteObject:element];
|
[self.fetchedResultsController.managedObjectContext deleteObject:element];
|
||||||
|
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointDeleteElement];
|
[TestFlight passCheckpoint:MPCheckpointDeleteElement];
|
||||||
|
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointDeleteElement
|
||||||
|
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
|
element.typeName, @"type",
|
||||||
|
PearlUnsignedInteger(element.version), @"version",
|
||||||
|
nil]];
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
@protocol MPTypeDelegate <NSObject>
|
@protocol MPTypeDelegate<NSObject>
|
||||||
|
|
||||||
- (void)didSelectType:(MPElementType)type;
|
- (void)didSelectType:(MPElementType)type;
|
||||||
|
|
||||||
@@ -20,5 +20,6 @@
|
|||||||
@interface MPTypeViewController : UITableViewController
|
@interface MPTypeViewController : UITableViewController
|
||||||
|
|
||||||
@property (nonatomic, weak) id<MPTypeDelegate> delegate;
|
@property (nonatomic, weak) id<MPTypeDelegate> delegate;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *recommendedTipContainer;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -17,9 +17,36 @@
|
|||||||
|
|
||||||
@implementation MPTypeViewController
|
@implementation MPTypeViewController
|
||||||
@synthesize delegate;
|
@synthesize delegate;
|
||||||
|
@synthesize recommendedTipContainer;
|
||||||
|
|
||||||
#pragma mark - View lifecycle
|
#pragma mark - View lifecycle
|
||||||
|
|
||||||
|
- (void)viewWillAppear:(BOOL)animated {
|
||||||
|
|
||||||
|
inf(@"Type selection will appear");
|
||||||
|
self.recommendedTipContainer.alpha = 0;
|
||||||
|
|
||||||
|
[super viewWillAppear:animated];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)viewDidAppear:(BOOL)animated {
|
||||||
|
|
||||||
|
if ([[MPiOSConfig get].firstRun boolValue])
|
||||||
|
[UIView animateWithDuration:animated? 0.3f: 0 animations:^{
|
||||||
|
self.recommendedTipContainer.alpha = 1;
|
||||||
|
} completion:^(BOOL finished) {
|
||||||
|
if (finished) {
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||||
|
[UIView animateWithDuration:0.2f animations:^{
|
||||||
|
self.recommendedTipContainer.alpha = 0;
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
|
||||||
|
[super viewDidAppear:animated];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)viewDidLoad {
|
- (void)viewDidLoad {
|
||||||
|
|
||||||
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"ui_background"]];
|
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"ui_background"]];
|
||||||
@@ -27,6 +54,12 @@
|
|||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)viewWillDisappear:(BOOL)animated {
|
||||||
|
|
||||||
|
inf(@"Type selection will disappear");
|
||||||
|
[super viewWillDisappear:animated];
|
||||||
|
}
|
||||||
|
|
||||||
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
|
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
|
||||||
|
|
||||||
return YES;
|
return YES;
|
||||||
@@ -37,18 +70,7 @@
|
|||||||
UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath];
|
UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath];
|
||||||
|
|
||||||
if ([delegate respondsToSelector:@selector(selectedType)])
|
if ([delegate respondsToSelector:@selector(selectedType)])
|
||||||
if ([delegate selectedType] == [self typeAtIndexPath:indexPath])
|
cell.selected = ([delegate selectedType] == [self typeAtIndexPath:indexPath]);
|
||||||
[cell iterateSubviewsContinueAfter:^BOOL(UIView *subview) {
|
|
||||||
if ([subview isKindOfClass:[UIImageView class]]) {
|
|
||||||
UIImageView *imageView = ((UIImageView *)subview);
|
|
||||||
if (!imageView.highlightedImage)
|
|
||||||
imageView.highlightedImage = [imageView.image highlightedImage];
|
|
||||||
imageView.highlighted = YES;
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
return YES;
|
|
||||||
}];
|
|
||||||
|
|
||||||
return cell;
|
return cell;
|
||||||
}
|
}
|
||||||
@@ -57,7 +79,12 @@
|
|||||||
|
|
||||||
assert(self.navigationController.topViewController == self);
|
assert(self.navigationController.topViewController == self);
|
||||||
|
|
||||||
[delegate didSelectType:[self typeAtIndexPath:indexPath]];
|
MPElementType type = [self typeAtIndexPath:indexPath];
|
||||||
|
if (type == NSNotFound)
|
||||||
|
// Selected a non-type row.
|
||||||
|
return;
|
||||||
|
|
||||||
|
[delegate didSelectType:type];
|
||||||
[self.navigationController popViewControllerAnimated:YES];
|
[self.navigationController popViewControllerAnimated:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,44 +95,54 @@
|
|||||||
// Generated
|
// Generated
|
||||||
switch (indexPath.row) {
|
switch (indexPath.row) {
|
||||||
case 0:
|
case 0:
|
||||||
return MPElementTypeGeneratedLong;
|
return NSNotFound;
|
||||||
case 1:
|
case 1:
|
||||||
return MPElementTypeGeneratedMedium;
|
return MPElementTypeGeneratedMaximum;
|
||||||
case 2:
|
case 2:
|
||||||
return MPElementTypeGeneratedShort;
|
return MPElementTypeGeneratedLong;
|
||||||
case 3:
|
case 3:
|
||||||
return MPElementTypeGeneratedBasic;
|
return MPElementTypeGeneratedMedium;
|
||||||
case 4:
|
case 4:
|
||||||
|
return MPElementTypeGeneratedShort;
|
||||||
|
case 5:
|
||||||
|
return MPElementTypeGeneratedBasic;
|
||||||
|
case 6:
|
||||||
return MPElementTypeGeneratedPIN;
|
return MPElementTypeGeneratedPIN;
|
||||||
|
case 7:
|
||||||
|
return NSNotFound;
|
||||||
|
|
||||||
default:
|
default: {
|
||||||
[NSException raise:NSInternalInconsistencyException
|
Throw(@"Unsupported row: %d, when selecting generated element type.", indexPath.row);
|
||||||
format:@"Unsupported row: %d, when selecting generated element type.", indexPath.row];
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 1: {
|
case 1: {
|
||||||
// Stored
|
// Stored
|
||||||
switch (indexPath.row) {
|
switch (indexPath.row) {
|
||||||
case 0:
|
case 0:
|
||||||
return MPElementTypeStoredPersonal;
|
return NSNotFound;
|
||||||
case 1:
|
case 1:
|
||||||
|
return MPElementTypeStoredPersonal;
|
||||||
|
case 2:
|
||||||
return MPElementTypeStoredDevicePrivate;
|
return MPElementTypeStoredDevicePrivate;
|
||||||
|
case 3:
|
||||||
|
return NSNotFound;
|
||||||
|
|
||||||
default:
|
default: {
|
||||||
[NSException raise:NSInternalInconsistencyException
|
Throw(@"Unsupported row: %d, when selecting stored element type.", indexPath.row);
|
||||||
format:@"Unsupported row: %d, when selecting stored element type.", indexPath.row];
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
[NSException raise:NSInternalInconsistencyException
|
Throw(@"Unsupported section: %d, when selecting element type.", indexPath.section);
|
||||||
format:@"Unsupported section: %d, when selecting element type.", indexPath.section];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@throw nil;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)viewDidUnload {
|
||||||
|
|
||||||
|
[self setRecommendedTipContainer:nil];
|
||||||
|
[super viewDidUnload];
|
||||||
|
}
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -8,14 +8,26 @@
|
|||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
@interface MPUnlockViewController : UIViewController <UITextFieldDelegate>
|
@interface MPUnlockViewController : UIViewController<UITextFieldDelegate, UIScrollViewDelegate>
|
||||||
|
|
||||||
@property (weak, nonatomic) IBOutlet UIImageView *lock;
|
@property (weak, nonatomic) IBOutlet UIImageView *spinner;
|
||||||
@property (weak, nonatomic) IBOutlet UIImageView *spinner;
|
@property (weak, nonatomic) IBOutlet UILabel *passwordFieldLabel;
|
||||||
@property (weak, nonatomic) IBOutlet UITextField *field;
|
@property (weak, nonatomic) IBOutlet UITextField *passwordField;
|
||||||
@property (weak, nonatomic) IBOutlet UILabel *messageLabel;
|
@property (weak, nonatomic) IBOutlet UIView *passwordView;
|
||||||
@property (weak, nonatomic) IBOutlet UIView *changeMPView;
|
@property (weak, nonatomic) IBOutlet UIScrollView *avatarsView;
|
||||||
|
@property (weak, nonatomic) IBOutlet UILabel *nameLabel;
|
||||||
|
@property (weak, nonatomic) IBOutlet UILabel *oldNameLabel;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIButton *avatarTemplate;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *createPasswordTipView;
|
||||||
|
@property (weak, nonatomic) IBOutlet UILabel *tip;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *passwordTipView;
|
||||||
|
@property (weak, nonatomic) IBOutlet UILabel *passwordTipLabel;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIView *wordWall;
|
||||||
|
@property (strong, nonatomic) IBOutlet UILongPressGestureRecognizer *targetedUserActionGesture;
|
||||||
|
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *loadingUsersIndicator;
|
||||||
|
|
||||||
- (IBAction)changeMP;
|
@property (nonatomic, strong) UIColor *avatarShadowColor;
|
||||||
|
|
||||||
|
- (IBAction)targetedUserAction:(UILongPressGestureRecognizer *)sender;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -12,77 +12,107 @@
|
|||||||
#import "MPAppDelegate.h"
|
#import "MPAppDelegate.h"
|
||||||
#import "MPAppDelegate_Key.h"
|
#import "MPAppDelegate_Key.h"
|
||||||
#import "MPAppDelegate_Store.h"
|
#import "MPAppDelegate_Store.h"
|
||||||
#import "MPElementEntity.h"
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
MPLockscreenIdle,
|
|
||||||
MPLockscreenError,
|
|
||||||
MPLockscreenSuccess,
|
|
||||||
MPLockscreenProgress,
|
|
||||||
} MPLockscreen;
|
|
||||||
|
|
||||||
@interface MPUnlockViewController ()
|
@interface MPUnlockViewController ()
|
||||||
|
|
||||||
|
@property (strong, nonatomic) MPUserEntity *selectedUser;
|
||||||
|
@property (strong, nonatomic) NSMutableDictionary *avatarToUser;
|
||||||
|
@property (nonatomic) BOOL wordWallAnimating;
|
||||||
|
@property (nonatomic, strong) NSArray *wordList;
|
||||||
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation MPUnlockViewController
|
@implementation MPUnlockViewController
|
||||||
@synthesize lock;
|
@synthesize selectedUser;
|
||||||
|
@synthesize avatarToUser;
|
||||||
@synthesize spinner;
|
@synthesize spinner;
|
||||||
@synthesize field;
|
@synthesize passwordFieldLabel;
|
||||||
@synthesize messageLabel;
|
@synthesize passwordField;
|
||||||
@synthesize changeMPView;
|
@synthesize passwordView;
|
||||||
|
@synthesize avatarsView;
|
||||||
|
@synthesize nameLabel, oldNameLabel;
|
||||||
|
@synthesize avatarTemplate;
|
||||||
|
@synthesize createPasswordTipView;
|
||||||
|
@synthesize tip;
|
||||||
|
@synthesize passwordTipView;
|
||||||
|
@synthesize passwordTipLabel;
|
||||||
|
@synthesize wordWall;
|
||||||
|
@synthesize targetedUserActionGesture;
|
||||||
|
@synthesize loadingUsersIndicator;
|
||||||
|
@synthesize avatarShadowColor = _avatarShadowColor;
|
||||||
|
@synthesize wordWallAnimating = _wordWallAnimating;
|
||||||
|
@synthesize wordList = _wordList;
|
||||||
|
|
||||||
- (void)showMessage:(NSString *)message state:(MPLockscreen)state {
|
|
||||||
|
|
||||||
__block void(^showMessageAnimation)(void) = ^{
|
- (void)initializeAvatarAlert:(UIAlertView *)alert forUser:(MPUserEntity *)user {
|
||||||
self.lock.alpha = 0.0f;
|
|
||||||
switch (state) {
|
|
||||||
case MPLockscreenIdle:
|
|
||||||
[self.lock setImage:[UIImage imageNamed:@"lock_idle"]];
|
|
||||||
break;
|
|
||||||
case MPLockscreenError:
|
|
||||||
[self.lock setImage:[UIImage imageNamed:@"lock_red"]];
|
|
||||||
break;
|
|
||||||
case MPLockscreenSuccess:
|
|
||||||
[self.lock setImage:[UIImage imageNamed:@"lock_green"]];
|
|
||||||
break;
|
|
||||||
case MPLockscreenProgress:
|
|
||||||
[self.lock setImage:[UIImage imageNamed:@"lock_blue"]];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.lock.alpha = 0.0f;
|
UIScrollView *alertAvatarScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(12, 30, 260, 150)];
|
||||||
[UIView animateWithDuration:1.0f animations:^{
|
alertAvatarScrollView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
|
||||||
self.lock.alpha = 1.0f;
|
[alertAvatarScrollView flashScrollIndicatorsContinuously];
|
||||||
} completion:^(BOOL finished) {
|
[alert addSubview:alertAvatarScrollView];
|
||||||
if (finished)
|
|
||||||
[UIView animateWithDuration:1.0f delay:0 options:UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse animations:^{
|
|
||||||
self.lock.alpha = 0.5f;
|
|
||||||
} completion:nil];
|
|
||||||
}];
|
|
||||||
|
|
||||||
[UIView animateWithDuration:0.5f animations:^{
|
CGPoint selectedOffset = CGPointZero;
|
||||||
self.messageLabel.alpha = 1.0f;
|
for (int a = 0; a < MPAvatarCount; ++a) {
|
||||||
self.messageLabel.text = message;
|
UIButton *avatar = [self.avatarTemplate cloneAddedTo:alertAvatarScrollView];
|
||||||
}];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (self.messageLabel.alpha)
|
avatar.tag = a;
|
||||||
[UIView animateWithDuration:0.3f animations:^{
|
avatar.hidden = NO;
|
||||||
self.messageLabel.alpha = 0.0f;
|
avatar.center = CGPointMake(
|
||||||
} completion:^(BOOL finished) {
|
(20 + self.avatarTemplate.bounds.size.width / 2) * (a + 1) + self.avatarTemplate.bounds.size.width / 2 * a,
|
||||||
if (finished)
|
20 + self.avatarTemplate.bounds.size.height / 2);
|
||||||
showMessageAnimation();
|
[avatar setBackgroundImage:[UIImage imageNamed:PearlString(@"avatar-%d", a)] forState:UIControlStateNormal];
|
||||||
}];
|
[avatar setSelectionInSuperviewCandidate:YES isClearable:NO];
|
||||||
else
|
|
||||||
showMessageAnimation();
|
avatar.layer.cornerRadius = avatar.bounds.size.height / 2;
|
||||||
|
avatar.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||||
|
avatar.layer.shadowOpacity = 1;
|
||||||
|
avatar.layer.shadowRadius = 5;
|
||||||
|
avatar.backgroundColor = [UIColor clearColor];
|
||||||
|
|
||||||
|
[avatar onHighlightOrSelect:^(BOOL highlighted, BOOL selected) {
|
||||||
|
if (highlighted || selected)
|
||||||
|
avatar.backgroundColor = self.avatarTemplate.backgroundColor;
|
||||||
|
else
|
||||||
|
avatar.backgroundColor = [UIColor clearColor];
|
||||||
|
} options:0];
|
||||||
|
[avatar onSelect:^(BOOL selected) {
|
||||||
|
if (selected)
|
||||||
|
user.avatar = (unsigned)avatar.tag;
|
||||||
|
} options:0];
|
||||||
|
avatar.selected = (a == user.avatar);
|
||||||
|
if (avatar.selected)
|
||||||
|
selectedOffset = CGPointMake(avatar.center.x - alertAvatarScrollView.bounds.size.width / 2, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[alertAvatarScrollView autoSizeContent];
|
||||||
|
[alertAvatarScrollView setContentOffset:selectedOffset animated:YES];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)hideMessage {
|
- (void)initializeConfirmationAlert:(UIAlertView *)alert forUser:(MPUserEntity *)user {
|
||||||
|
|
||||||
[UIView animateWithDuration:0.5f animations:^{
|
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(12, 70, 260, 110)];
|
||||||
self.messageLabel.alpha = 0.0f;
|
[alert addSubview:container];
|
||||||
}];
|
|
||||||
|
UIButton *alertAvatar = [self.avatarTemplate cloneAddedTo:container];
|
||||||
|
alertAvatar.center = CGPointMake(130, 55);
|
||||||
|
alertAvatar.hidden = NO;
|
||||||
|
alertAvatar.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||||
|
alertAvatar.layer.shadowOpacity = 1;
|
||||||
|
alertAvatar.layer.shadowRadius = 5;
|
||||||
|
alertAvatar.backgroundColor = [UIColor clearColor];
|
||||||
|
[alertAvatar setBackgroundImage:[UIImage imageNamed:PearlString(@"avatar-%d", user.avatar)] forState:UIControlStateNormal];
|
||||||
|
|
||||||
|
UILabel *alertNameLabel = [self.nameLabel cloneAddedTo:container];
|
||||||
|
alertNameLabel.center = alertAvatar.center;
|
||||||
|
alertNameLabel.text = user.name;
|
||||||
|
alertNameLabel.bounds = CGRectSetHeight(alertNameLabel.bounds,
|
||||||
|
[alertNameLabel.text sizeWithFont:self.nameLabel.font
|
||||||
|
constrainedToSize:CGSizeMake(alertNameLabel.bounds.size.width - 10,
|
||||||
|
100)
|
||||||
|
lineBreakMode:self.nameLabel.lineBreakMode].height);
|
||||||
|
alertNameLabel.layer.cornerRadius = 5;
|
||||||
|
alertNameLabel.backgroundColor = [UIColor blackColor];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
|
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
|
||||||
@@ -92,16 +122,40 @@ typedef enum {
|
|||||||
|
|
||||||
- (void)viewDidLoad {
|
- (void)viewDidLoad {
|
||||||
|
|
||||||
self.messageLabel.text = nil;
|
self.avatarToUser = [NSMutableDictionary dictionaryWithCapacity:3];
|
||||||
self.messageLabel.alpha = 0;
|
|
||||||
self.changeMPView.alpha = 0;
|
|
||||||
self.spinner.alpha = 0;
|
|
||||||
self.field.text = nil;
|
|
||||||
|
|
||||||
[[NSNotificationCenter defaultCenter] addObserverForName:MPNotificationKeyForgotten
|
self.avatarsView.decelerationRate = UIScrollViewDecelerationRateFast;
|
||||||
object:nil queue:nil usingBlock:^(NSNotification *note) {
|
self.avatarsView.clipsToBounds = NO;
|
||||||
[self.field becomeFirstResponder];
|
self.nameLabel.layer.cornerRadius = 5;
|
||||||
}];
|
self.avatarTemplate.hidden = YES;
|
||||||
|
self.spinner.alpha = 0;
|
||||||
|
self.passwordTipView.alpha = 0;
|
||||||
|
self.createPasswordTipView.alpha = 0;
|
||||||
|
|
||||||
|
NSMutableArray *wordListLines = [NSMutableArray arrayWithCapacity:27413];
|
||||||
|
[[[NSString alloc] initWithData:[NSData dataWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"dictionary" withExtension:@"lst"]]
|
||||||
|
encoding:NSUTF8StringEncoding] enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {
|
||||||
|
[wordListLines addObject:line];
|
||||||
|
}];
|
||||||
|
self.wordList = wordListLines;
|
||||||
|
|
||||||
|
self.wordWall.alpha = 0;
|
||||||
|
[self.wordWall enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
|
||||||
|
UILabel *wordLabel = (UILabel *)subview;
|
||||||
|
|
||||||
|
[self initializeWordLabel:wordLabel];
|
||||||
|
} recurse:NO];
|
||||||
|
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserverForName:PersistentStoreDidChange object:nil queue:nil usingBlock:
|
||||||
|
^(NSNotification *note) {
|
||||||
|
[self updateUsers];
|
||||||
|
}];
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserverForName:PersistentStoreDidMergeChanges object:nil queue:nil usingBlock:
|
||||||
|
^(NSNotification *note) {
|
||||||
|
[self updateUsers];
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self updateLayoutAnimated:NO allowScroll:YES completion:nil];
|
||||||
|
|
||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
}
|
}
|
||||||
@@ -109,148 +163,600 @@ typedef enum {
|
|||||||
- (void)viewDidUnload {
|
- (void)viewDidUnload {
|
||||||
|
|
||||||
[self setSpinner:nil];
|
[self setSpinner:nil];
|
||||||
[self setField:nil];
|
[self setPasswordField:nil];
|
||||||
|
[self setPasswordView:nil];
|
||||||
[self setMessageLabel:nil];
|
[self setAvatarsView:nil];
|
||||||
[self setLock:nil];
|
[self setNameLabel:nil];
|
||||||
[self setChangeMPView:nil];
|
[self setAvatarTemplate:nil];
|
||||||
|
[self setTip:nil];
|
||||||
|
[self setPasswordTipView:nil];
|
||||||
|
[self setPasswordTipLabel:nil];
|
||||||
|
[self setTargetedUserActionGesture:nil];
|
||||||
|
[self setWordWall:nil];
|
||||||
|
[self setCreatePasswordTipView:nil];
|
||||||
|
[self setPasswordFieldLabel:nil];
|
||||||
|
[self setLoadingUsersIndicator:nil];
|
||||||
[super viewDidUnload];
|
[super viewDidUnload];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewWillAppear:(BOOL)animated {
|
- (void)viewWillAppear:(BOOL)animated {
|
||||||
|
|
||||||
[[UIApplication sharedApplication] setStatusBarHidden:YES
|
inf(@"Lock screen will appear");
|
||||||
withAnimation:animated? UIStatusBarAnimationSlide: UIStatusBarAnimationNone];
|
self.selectedUser = nil;
|
||||||
|
[self updateUsers];
|
||||||
|
|
||||||
[super viewWillAppear:animated];
|
[super viewWillAppear:animated];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewWillDisappear:(BOOL)animated {
|
|
||||||
|
|
||||||
[[UIApplication sharedApplication] setStatusBarHidden:NO
|
|
||||||
withAnimation:animated? UIStatusBarAnimationSlide: UIStatusBarAnimationNone];
|
|
||||||
|
|
||||||
[super viewWillDisappear:animated];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)viewDidAppear:(BOOL)animated {
|
- (void)viewDidAppear:(BOOL)animated {
|
||||||
|
|
||||||
[self.field becomeFirstResponder];
|
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:animated? UIStatusBarAnimationSlide: UIStatusBarAnimationNone];
|
||||||
|
|
||||||
|
if (!animated)
|
||||||
|
[[self findTargetedAvatar] setSelected:YES];
|
||||||
|
|
||||||
[super viewDidAppear:animated];
|
[super viewDidAppear:animated];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
- (void)viewWillDisappear:(BOOL)animated {
|
||||||
|
|
||||||
if ([textField.text length]) {
|
inf(@"Lock screen will disappear");
|
||||||
[textField resignFirstResponder];
|
[super viewWillDisappear:animated];
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NO;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)textFieldDidEndEditing:(UITextField *)textField {
|
- (void)updateUsers {
|
||||||
|
|
||||||
|
NSManagedObjectContext *moc = [MPAppDelegate managedObjectContextIfReady];
|
||||||
|
if (!moc) {
|
||||||
|
self.tip.text = @"Loading...";
|
||||||
|
[self.loadingUsersIndicator startAnimating];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tip.text = @"Tap and hold to delete or reset.";
|
||||||
|
[self.loadingUsersIndicator stopAnimating];
|
||||||
|
|
||||||
|
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPUserEntity class])];
|
||||||
|
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"lastUsed" ascending:NO]];
|
||||||
|
NSArray *users = [moc executeFetchRequest:fetchRequest error:nil];
|
||||||
|
|
||||||
|
// Clean up avatars.
|
||||||
|
for (UIView *subview in [self.avatarsView subviews])
|
||||||
|
if ([[self.avatarToUser allKeys] containsObject:[NSValue valueWithNonretainedObject:subview]])
|
||||||
|
// This subview is a former avatar.
|
||||||
|
[subview removeFromSuperview];
|
||||||
|
[self.avatarToUser removeAllObjects];
|
||||||
|
|
||||||
|
// Create avatars.
|
||||||
|
for (MPUserEntity *user in users)
|
||||||
|
[self setupAvatar:[self.avatarTemplate clone] forUser:user];
|
||||||
|
[self setupAvatar:[self.avatarTemplate clone] forUser:nil];
|
||||||
|
|
||||||
|
// Scroll view's content changed, update its content size.
|
||||||
|
[self.avatarsView autoSizeContentIgnoreHidden:YES ignoreInvisible:YES limitPadding:NO ignoreSubviews:nil];
|
||||||
|
|
||||||
|
[self updateLayoutAnimated:YES allowScroll:YES completion:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIButton *)setupAvatar:(UIButton *)avatar forUser:(MPUserEntity *)user {
|
||||||
|
|
||||||
|
avatar.center = CGPointMake(avatar.center.x + [self.avatarToUser count] * 160, avatar.center.y);
|
||||||
|
avatar.hidden = NO;
|
||||||
|
avatar.layer.cornerRadius = avatar.bounds.size.height / 2;
|
||||||
|
avatar.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||||
|
avatar.layer.shadowOpacity = 1;
|
||||||
|
avatar.layer.shadowRadius = 20;
|
||||||
|
avatar.layer.masksToBounds = NO;
|
||||||
|
avatar.backgroundColor = [UIColor clearColor];
|
||||||
|
avatar.tag = user.avatar;
|
||||||
|
|
||||||
|
[avatar setBackgroundImage:[UIImage imageNamed:PearlString(@"avatar-%u", user.avatar)]
|
||||||
|
forState:UIControlStateNormal];
|
||||||
|
[avatar setSelectionInSuperviewCandidate:YES isClearable:YES];
|
||||||
|
[avatar onHighlightOrSelect:^(BOOL highlighted, BOOL selected) {
|
||||||
|
if (highlighted || selected)
|
||||||
|
avatar.backgroundColor = self.avatarTemplate.backgroundColor;
|
||||||
|
else
|
||||||
|
avatar.backgroundColor = [UIColor clearColor];
|
||||||
|
} options:0];
|
||||||
|
[avatar onSelect:^(BOOL selected) {
|
||||||
|
if (selected) {
|
||||||
|
if ((self.selectedUser = user))
|
||||||
|
[self didToggleUserSelection];
|
||||||
|
else
|
||||||
|
[self didSelectNewUserAvatar:avatar];
|
||||||
|
} else {
|
||||||
|
self.selectedUser = nil;
|
||||||
|
[self didToggleUserSelection];
|
||||||
|
}
|
||||||
|
} options:0];
|
||||||
|
|
||||||
|
[self.avatarToUser setObject:NilToNSNull(user) forKey:[NSValue valueWithNonretainedObject:avatar]];
|
||||||
|
|
||||||
|
if (self.selectedUser && user == self.selectedUser)
|
||||||
|
avatar.selected = YES;
|
||||||
|
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)didToggleUserSelection {
|
||||||
|
|
||||||
|
if (!self.selectedUser)
|
||||||
|
[self.passwordField resignFirstResponder];
|
||||||
|
else
|
||||||
|
if ([[MPAppDelegate get] signInAsUser:self.selectedUser usingMasterPassword:nil]) {
|
||||||
|
[self dismissViewControllerAnimated:YES completion:nil];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self updateLayoutAnimated:YES allowScroll:YES completion:^(BOOL finished) {
|
||||||
|
if (finished)
|
||||||
|
if (self.selectedUser)
|
||||||
|
[self.passwordField becomeFirstResponder];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)didSelectNewUserAvatar:(UIButton *)newUserAvatar {
|
||||||
|
|
||||||
|
__block MPUserEntity *newUser = nil;
|
||||||
|
[[MPAppDelegate managedObjectContextIfReady] performBlockAndWait:^{
|
||||||
|
newUser = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPUserEntity class])
|
||||||
|
inManagedObjectContext:[MPAppDelegate managedObjectContextIfReady]];
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self showNewUserNameAlertFor:newUser completion:^(BOOL finished) {
|
||||||
|
newUserAvatar.selected = NO;
|
||||||
|
if (!finished)
|
||||||
|
[[MPAppDelegate managedObjectContextIfReady] performBlock:^{
|
||||||
|
[[MPAppDelegate managedObjectContextIfReady] deleteObject:newUser];
|
||||||
|
}];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)showNewUserNameAlertFor:(MPUserEntity *)newUser completion:(void (^)(BOOL finished))completion {
|
||||||
|
|
||||||
|
[PearlAlert showAlertWithTitle:@"Enter Your Name"
|
||||||
|
message:nil viewStyle:UIAlertViewStylePlainTextInput
|
||||||
|
initAlert:^(UIAlertView *alert, UITextField *firstField) {
|
||||||
|
firstField.autocapitalizationType = UITextAutocapitalizationTypeWords;
|
||||||
|
firstField.keyboardType = UIKeyboardTypeAlphabet;
|
||||||
|
firstField.text = newUser.name;
|
||||||
|
firstField.placeholder = @"eg. Robert Lee Mitchell";
|
||||||
|
}
|
||||||
|
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||||
|
if (buttonIndex == [alert cancelButtonIndex]) {
|
||||||
|
completion(NO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
newUser.name = [alert textFieldAtIndex:0].text;
|
||||||
|
[self showNewUserAvatarAlertFor:newUser completion:completion];
|
||||||
|
}
|
||||||
|
cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonSave, nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)showNewUserAvatarAlertFor:(MPUserEntity *)newUser completion:(void (^)(BOOL finished))completion {
|
||||||
|
|
||||||
|
[PearlAlert showAlertWithTitle:@"Choose Your Avatar"
|
||||||
|
message:@"\n\n\n\n\n\n" viewStyle:UIAlertViewStyleDefault
|
||||||
|
initAlert:^(UIAlertView *_alert, UITextField *_firstField) {
|
||||||
|
[self initializeAvatarAlert:_alert forUser:newUser];
|
||||||
|
}
|
||||||
|
tappedButtonBlock:^(UIAlertView *_alert, NSInteger _buttonIndex) {
|
||||||
|
|
||||||
|
// Okay
|
||||||
|
[self showNewUserConfirmationAlertFor:newUser completion:completion];
|
||||||
|
} cancelTitle:nil otherTitles:[PearlStrings get].commonButtonOkay, nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)showNewUserConfirmationAlertFor:(MPUserEntity *)newUser completion:(void (^)(BOOL finished))completion {
|
||||||
|
|
||||||
|
[PearlAlert showAlertWithTitle:@"Is this correct?"
|
||||||
|
message:
|
||||||
|
@"Please double-check your name.\n"
|
||||||
|
@"\n\n\n\n\n\n"
|
||||||
|
viewStyle:UIAlertViewStyleDefault
|
||||||
|
initAlert:^void(UIAlertView *__alert, UITextField *__firstField) {
|
||||||
|
[self initializeConfirmationAlert:__alert forUser:newUser];
|
||||||
|
}
|
||||||
|
tappedButtonBlock:^void(UIAlertView *__alert, NSInteger __buttonIndex) {
|
||||||
|
if (__buttonIndex == [__alert cancelButtonIndex]) {
|
||||||
|
[self showNewUserNameAlertFor:newUser completion:completion];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm
|
||||||
|
completion(YES);
|
||||||
|
self.selectedUser = newUser;
|
||||||
|
|
||||||
|
[self updateUsers];
|
||||||
|
}
|
||||||
|
cancelTitle:@"Change" otherTitles:@"Confirm", nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)updateLayoutAnimated:(BOOL)animated allowScroll:(BOOL)allowScroll completion:(void (^)(BOOL finished))completion {
|
||||||
|
|
||||||
|
if (animated) {
|
||||||
|
self.oldNameLabel.text = self.nameLabel.text;
|
||||||
|
self.oldNameLabel.alpha = 1;
|
||||||
|
self.nameLabel.alpha = 0;
|
||||||
|
|
||||||
|
[UIView animateWithDuration:0.5f animations:^{
|
||||||
|
[self updateLayoutAnimated:NO allowScroll:allowScroll completion:nil];
|
||||||
|
|
||||||
|
self.oldNameLabel.alpha = 0;
|
||||||
|
self.nameLabel.alpha = 1;
|
||||||
|
} completion:^(BOOL finished) {
|
||||||
|
if (completion)
|
||||||
|
completion(finished);
|
||||||
|
}];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lay out password entry and user selection views.
|
||||||
|
if (self.selectedUser && !self.passwordView.alpha) {
|
||||||
|
// User was just selected.
|
||||||
|
self.passwordView.alpha = 1;
|
||||||
|
self.avatarsView.center = CGPointMake(160, 170);
|
||||||
|
self.avatarsView.scrollEnabled = NO;
|
||||||
|
self.nameLabel.center = CGPointMake(160, 84);
|
||||||
|
self.nameLabel.backgroundColor = [UIColor blackColor];
|
||||||
|
self.oldNameLabel.center = self.nameLabel.center;
|
||||||
|
self.avatarShadowColor = [UIColor whiteColor];
|
||||||
|
} else
|
||||||
|
if (!self.selectedUser && self.passwordView.alpha == 1) {
|
||||||
|
// User was just deselected.
|
||||||
|
self.passwordField.text = nil;
|
||||||
|
self.passwordView.alpha = 0;
|
||||||
|
self.avatarsView.center = CGPointMake(160, 310);
|
||||||
|
self.avatarsView.scrollEnabled = YES;
|
||||||
|
self.nameLabel.center = CGPointMake(160, 296);
|
||||||
|
self.nameLabel.backgroundColor = [UIColor clearColor];
|
||||||
|
self.oldNameLabel.center = self.nameLabel.center;
|
||||||
|
self.avatarShadowColor = [UIColor lightGrayColor];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lay out the word wall.
|
||||||
|
if (!self.selectedUser || self.selectedUser.keyID) {
|
||||||
|
self.passwordFieldLabel.text = @"Enter your master password:";
|
||||||
|
|
||||||
|
self.wordWall.alpha = 0;
|
||||||
|
self.createPasswordTipView.alpha = 0;
|
||||||
|
self.wordWallAnimating = NO;
|
||||||
|
} else {
|
||||||
|
self.passwordFieldLabel.text = @"Create your master password:";
|
||||||
|
|
||||||
|
if (!self.wordWallAnimating) {
|
||||||
|
self.wordWallAnimating = YES;
|
||||||
|
self.wordWall.alpha = 1;
|
||||||
|
self.createPasswordTipView.alpha = 1;
|
||||||
|
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
// Jump out of our UIView animation block.
|
||||||
|
[self beginWordWallAnimation];
|
||||||
|
});
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
|
||||||
|
[UIView animateWithDuration:1 animations:^{
|
||||||
|
self.createPasswordTipView.alpha = 0;
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lay out user targeting.
|
||||||
|
MPUserEntity *targetedUser = self.selectedUser;
|
||||||
|
UIButton *selectedAvatar = [self avatarForUser:self.selectedUser];
|
||||||
|
UIButton *targetedAvatar = selectedAvatar;
|
||||||
|
if (!targetedAvatar) {
|
||||||
|
targetedAvatar = [self findTargetedAvatar];
|
||||||
|
targetedUser = [self userForAvatar:targetedAvatar];
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.avatarsView enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
|
||||||
|
if (![[self.avatarToUser allKeys] containsObject:[NSValue valueWithNonretainedObject:subview]])
|
||||||
|
// This subview is not one of the user avatars.
|
||||||
|
return;
|
||||||
|
UIButton *avatar = (UIButton *)subview;
|
||||||
|
|
||||||
|
BOOL isTargeted = avatar == targetedAvatar;
|
||||||
|
|
||||||
|
avatar.userInteractionEnabled = isTargeted;
|
||||||
|
avatar.alpha = isTargeted? 1: self.selectedUser? 0.1: 0.4;
|
||||||
|
|
||||||
|
[self updateAvatarShadowColor:avatar isTargeted:isTargeted];
|
||||||
|
} recurse:NO];
|
||||||
|
|
||||||
|
if (allowScroll) {
|
||||||
|
CGPoint targetContentOffset = CGPointMake(MAX(0, targetedAvatar.center.x - self.avatarsView.bounds.size.width / 2),
|
||||||
|
self.avatarsView.contentOffset.y);
|
||||||
|
if (!CGPointEqualToPoint(self.avatarsView.contentOffset, targetContentOffset))
|
||||||
|
[self.avatarsView setContentOffset:targetContentOffset animated:animated];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lay out user name label.
|
||||||
|
self.nameLabel.text = targetedAvatar? targetedUser? targetedUser.name: @"New User": nil;
|
||||||
|
self.nameLabel.bounds = CGRectSetHeight(self.nameLabel.bounds,
|
||||||
|
[self.nameLabel.text sizeWithFont:self.nameLabel.font
|
||||||
|
constrainedToSize:CGSizeMake(self.nameLabel.bounds.size.width - 10, 100)
|
||||||
|
lineBreakMode:self.nameLabel.lineBreakMode].height);
|
||||||
|
self.oldNameLabel.bounds = self.nameLabel.bounds;
|
||||||
|
if (completion)
|
||||||
|
completion(YES);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)beginWordWallAnimation {
|
||||||
|
|
||||||
|
[self.wordWall enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
|
||||||
|
UILabel *wordLabel = (UILabel *)subview;
|
||||||
|
|
||||||
|
if (wordLabel.frame.origin.x < -self.wordWall.frame.size.width / 3) {
|
||||||
|
wordLabel.frame = CGRectSetX(wordLabel.frame, wordLabel.frame.origin.x + self.wordWall.frame.size.width);
|
||||||
|
[self initializeWordLabel:wordLabel];
|
||||||
|
}
|
||||||
|
} recurse:NO];
|
||||||
|
|
||||||
|
if (self.wordWallAnimating)
|
||||||
|
[UIView animateWithDuration:15 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
|
||||||
|
[self.wordWall enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
|
||||||
|
UILabel *wordLabel = (UILabel *)subview;
|
||||||
|
|
||||||
|
wordLabel.frame = CGRectSetX(wordLabel.frame, wordLabel.frame.origin.x - self.wordWall.frame.size.width / 3);
|
||||||
|
} recurse:NO];
|
||||||
|
} completion:^(BOOL finished) {
|
||||||
|
if (finished)
|
||||||
|
[self beginWordWallAnimation];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)initializeWordLabel:(UILabel *)wordLabel {
|
||||||
|
|
||||||
|
wordLabel.alpha = 0.05 + (random() % 35) / 100.0F;
|
||||||
|
wordLabel.text = [self.wordList objectAtIndex:(NSUInteger)random() % [self.wordList count]];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setPasswordTip:(NSString *)string {
|
||||||
|
|
||||||
|
if (string.length)
|
||||||
|
self.passwordTipLabel.text = string;
|
||||||
|
|
||||||
|
[UIView animateWithDuration:0.3f animations:^{
|
||||||
|
self.passwordTipView.alpha = string.length? 1: 0;
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)tryMasterPassword {
|
||||||
|
|
||||||
|
[self setSpinnerActive:YES];
|
||||||
|
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
@try {
|
BOOL unlocked = [[MPAppDelegate get] signInAsUser:self.selectedUser usingMasterPassword:self.passwordField.text];
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
|
|
||||||
rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
|
|
||||||
rotate.fromValue = [NSNumber numberWithFloat:0];
|
|
||||||
rotate.toValue = [NSNumber numberWithFloat:2 * M_PI];
|
|
||||||
rotate.repeatCount = MAXFLOAT;
|
|
||||||
rotate.duration = 3.0;
|
|
||||||
|
|
||||||
[self.spinner.layer removeAllAnimations];
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
[self.spinner.layer addAnimation:rotate forKey:@"transform"];
|
if (unlocked)
|
||||||
|
[self dismissViewControllerAnimated:YES completion:nil];
|
||||||
|
|
||||||
[UIView animateWithDuration:0.3f animations:^{
|
else {
|
||||||
self.spinner.alpha = 1.0f;
|
if (self.passwordField.text.length)
|
||||||
}];
|
[self setPasswordTip:@"Incorrect password."];
|
||||||
|
|
||||||
[self showMessage:@"Checking password..." state:MPLockscreenProgress];
|
[self setSpinnerActive:NO];
|
||||||
});
|
}
|
||||||
|
});
|
||||||
if ([[MPAppDelegate get] tryMasterPassword:textField.text])
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[self showMessage:@"Success!" state:MPLockscreenSuccess];
|
|
||||||
|
|
||||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
|
||||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"keyID == %@", [MPAppDelegate get].keyID];
|
|
||||||
fetchRequest.fetchLimit = 1;
|
|
||||||
BOOL keyIDHasElements = [[[MPAppDelegate managedObjectContext] executeFetchRequest:fetchRequest error:nil] count] > 0;
|
|
||||||
if (keyIDHasElements)
|
|
||||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (long)(NSEC_PER_SEC * 1.5f)), dispatch_get_main_queue(), ^{
|
|
||||||
[self dismissModalViewControllerAnimated:YES];
|
|
||||||
});
|
|
||||||
else {
|
|
||||||
[PearlAlert showAlertWithTitle:@"New Master Password"
|
|
||||||
message:
|
|
||||||
@"Please confirm the spelling of this new master password."
|
|
||||||
viewStyle:UIAlertViewStyleSecureTextInput
|
|
||||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
|
||||||
if (buttonIndex == [alert cancelButtonIndex]) {
|
|
||||||
[[MPAppDelegate get] updateKey:nil];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (![[alert textFieldAtIndex:0].text isEqualToString:textField.text]) {
|
|
||||||
[PearlAlert showAlertWithTitle:@"Incorrect Master Password"
|
|
||||||
message:
|
|
||||||
@"The password you entered doesn't match with the master password you tried to use. "
|
|
||||||
@"You've probably mistyped one of them.\n\n"
|
|
||||||
@"Give it another try."
|
|
||||||
viewStyle:UIAlertViewStyleDefault tappedButtonBlock:nil
|
|
||||||
cancelTitle:[PearlStrings get].commonButtonOkay otherTitles:nil];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
[self dismissModalViewControllerAnimated:YES];
|
|
||||||
}
|
|
||||||
cancelTitle:[PearlStrings get].commonButtonCancel
|
|
||||||
otherTitles:[PearlStrings get].commonButtonContinue, nil];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
else
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[self showMessage:@"Not valid." state:MPLockscreenError];
|
|
||||||
[UIView animateWithDuration:0.5f animations:^{
|
|
||||||
self.changeMPView.alpha = 1.0f;
|
|
||||||
}];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@finally {
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
[UIView animateWithDuration:0.3f animations:^{
|
|
||||||
self.spinner.alpha = 0.0f;
|
|
||||||
} completion:^(BOOL finished) {
|
|
||||||
[self.spinner.layer removeAllAnimations];
|
|
||||||
}];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (IBAction)changeMP {
|
- (UIButton *)findTargetedAvatar {
|
||||||
|
|
||||||
[PearlAlert showAlertWithTitle:@"Changing Master Password"
|
CGFloat xOfMiddle = self.avatarsView.contentOffset.x + self.avatarsView.bounds.size.width / 2;
|
||||||
message:
|
return (UIButton *)[PearlUIUtils viewClosestTo:CGPointMake(xOfMiddle, self.avatarsView.contentOffset.y)
|
||||||
@"This will allow you to log in with a different master password.\n\n"
|
ofArray:self.avatarsView.subviews];
|
||||||
@"Note that you will only see the sites and passwords for the master password you log in with.\n"
|
|
||||||
@"If you log in with a different master password, your current sites will be unavailable.\n\n"
|
|
||||||
@"You can always change back to your current master password later.\n"
|
|
||||||
@"Your current sites and passwords will then become available again."
|
|
||||||
viewStyle:UIAlertViewStyleDefault
|
|
||||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
|
||||||
if (buttonIndex == [alert cancelButtonIndex])
|
|
||||||
return;
|
|
||||||
|
|
||||||
[[MPAppDelegate get] forgetKey];
|
|
||||||
[[MPAppDelegate get] loadKey:YES];
|
|
||||||
|
|
||||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPChanged];
|
|
||||||
}
|
|
||||||
cancelTitle:[PearlStrings get].commonButtonAbort
|
|
||||||
otherTitles:[PearlStrings get].commonButtonContinue, nil];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (UIButton *)avatarForUser:(MPUserEntity *)user {
|
||||||
|
|
||||||
|
__block UIButton *avatar = nil;
|
||||||
|
if (user)
|
||||||
|
[self.avatarToUser enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
|
||||||
|
if (obj == user)
|
||||||
|
avatar = [key nonretainedObjectValue];
|
||||||
|
}];
|
||||||
|
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (MPUserEntity *)userForAvatar:(UIButton *)avatar {
|
||||||
|
|
||||||
|
return NSNullToNil([self.avatarToUser objectForKey:[NSValue valueWithNonretainedObject:avatar]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setSpinnerActive:(BOOL)active {
|
||||||
|
|
||||||
|
PearlMainThread(^{
|
||||||
|
CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
|
||||||
|
rotate.toValue = [NSNumber numberWithDouble:2 * M_PI];
|
||||||
|
rotate.duration = 5.0;
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
|
||||||
|
rotate.fromValue = [NSNumber numberWithFloat:0];
|
||||||
|
rotate.repeatCount = MAXFLOAT;
|
||||||
|
} else {
|
||||||
|
rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
|
||||||
|
rotate.repeatCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.spinner.layer removeAnimationForKey:@"rotation"];
|
||||||
|
[self.spinner.layer addAnimation:rotate forKey:@"rotation"];
|
||||||
|
|
||||||
|
[UIView animateWithDuration:0.3f animations:^{
|
||||||
|
self.spinner.alpha = active? 1: 0;
|
||||||
|
|
||||||
|
if (active)
|
||||||
|
[self avatarForUser:self.selectedUser].backgroundColor = [UIColor clearColor];
|
||||||
|
else
|
||||||
|
[self avatarForUser:self.selectedUser].backgroundColor = self.avatarTemplate.backgroundColor;
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)updateAvatarShadowColor:(UIButton *)avatar isTargeted:(BOOL)targeted {
|
||||||
|
|
||||||
|
if (targeted) {
|
||||||
|
if (![avatar.layer animationForKey:@"targetedShadow"]) {
|
||||||
|
CABasicAnimation *toShadowColorAnimation = [CABasicAnimation animationWithKeyPath:@"shadowColor"];
|
||||||
|
toShadowColorAnimation.toValue = (__bridge id)(avatar.selected? self.avatarTemplate.backgroundColor
|
||||||
|
: [UIColor whiteColor]).CGColor;
|
||||||
|
toShadowColorAnimation.beginTime = 0.0f;
|
||||||
|
toShadowColorAnimation.duration = 0.5f;
|
||||||
|
toShadowColorAnimation.fillMode = kCAFillModeForwards;
|
||||||
|
|
||||||
|
CABasicAnimation *toShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
|
||||||
|
toShadowOpacityAnimation.toValue = PearlFloat(0.2);
|
||||||
|
toShadowOpacityAnimation.duration = 0.5f;
|
||||||
|
|
||||||
|
CABasicAnimation *pulseShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
|
||||||
|
pulseShadowOpacityAnimation.fromValue = PearlFloat(0.2);
|
||||||
|
pulseShadowOpacityAnimation.toValue = PearlFloat(0.6);
|
||||||
|
pulseShadowOpacityAnimation.beginTime = 0.5f;
|
||||||
|
pulseShadowOpacityAnimation.duration = 2.0f;
|
||||||
|
pulseShadowOpacityAnimation.autoreverses = YES;
|
||||||
|
pulseShadowOpacityAnimation.repeatCount = MAXFLOAT;
|
||||||
|
|
||||||
|
CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
|
||||||
|
group.animations = [NSArray arrayWithObjects:toShadowColorAnimation, toShadowOpacityAnimation, pulseShadowOpacityAnimation,
|
||||||
|
nil];
|
||||||
|
group.duration = MAXFLOAT;
|
||||||
|
|
||||||
|
[avatar.layer removeAnimationForKey:@"inactiveShadow"];
|
||||||
|
[avatar.layer addAnimation:group forKey:@"targetedShadow"];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ([avatar.layer animationForKey:@"targetedShadow"]) {
|
||||||
|
CABasicAnimation *toShadowColorAnimation = [CABasicAnimation animationWithKeyPath:@"shadowColor"];
|
||||||
|
toShadowColorAnimation.toValue = (__bridge id)[UIColor blackColor].CGColor;
|
||||||
|
toShadowColorAnimation.duration = 0.5f;
|
||||||
|
|
||||||
|
CABasicAnimation *toShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
|
||||||
|
toShadowOpacityAnimation.toValue = PearlFloat(1);
|
||||||
|
toShadowOpacityAnimation.duration = 0.5f;
|
||||||
|
|
||||||
|
CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
|
||||||
|
group.animations = [NSArray arrayWithObjects:toShadowColorAnimation, toShadowOpacityAnimation, nil];
|
||||||
|
group.duration = 0.5f;
|
||||||
|
|
||||||
|
[avatar.layer removeAnimationForKey:@"targetedShadow"];
|
||||||
|
[avatar.layer addAnimation:group forKey:@"inactiveShadow"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - UITextFieldDelegate
|
||||||
|
|
||||||
|
- (void)textFieldDidBeginEditing:(UITextField *)textField {
|
||||||
|
|
||||||
|
[self setPasswordTip:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
||||||
|
|
||||||
|
[textField resignFirstResponder];
|
||||||
|
|
||||||
|
[self setSpinnerActive:YES];
|
||||||
|
|
||||||
|
if (self.selectedUser.keyID)
|
||||||
|
[self tryMasterPassword];
|
||||||
|
|
||||||
|
else
|
||||||
|
[PearlAlert showAlertWithTitle:@"New Master Password"
|
||||||
|
message:@"Please confirm the spelling of this new master password."
|
||||||
|
viewStyle:UIAlertViewStyleSecureTextInput
|
||||||
|
initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||||
|
[self setSpinnerActive:NO];
|
||||||
|
|
||||||
|
if (buttonIndex == [alert cancelButtonIndex])
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (![[alert textFieldAtIndex:0].text isEqualToString:textField.text]) {
|
||||||
|
[PearlAlert showAlertWithTitle:@"Incorrect Master Password"
|
||||||
|
message:
|
||||||
|
@"The password you entered doesn't match with the master password you tried to use. "
|
||||||
|
@"You've probably mistyped one of them.\n\n"
|
||||||
|
@"Give it another try."
|
||||||
|
viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:nil
|
||||||
|
cancelTitle:[PearlStrings get].commonButtonOkay otherTitles:nil];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self tryMasterPassword];
|
||||||
|
}
|
||||||
|
cancelTitle:[PearlStrings get].commonButtonCancel
|
||||||
|
otherTitles:[PearlStrings get].commonButtonContinue, nil];
|
||||||
|
|
||||||
|
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - UIScrollViewDelegate
|
||||||
|
|
||||||
|
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity
|
||||||
|
targetContentOffset:(inout CGPoint *)targetContentOffset {
|
||||||
|
|
||||||
|
CGFloat xOfMiddle = targetContentOffset->x + scrollView.bounds.size.width / 2;
|
||||||
|
UIButton *middleAvatar = (UIButton *)[PearlUIUtils viewClosestTo:CGPointMake(xOfMiddle, targetContentOffset->y)
|
||||||
|
ofArray:scrollView.subviews];
|
||||||
|
*targetContentOffset = CGPointMake(middleAvatar.center.x - scrollView.bounds.size.width / 2, targetContentOffset->y);
|
||||||
|
|
||||||
|
[self updateLayoutAnimated:NO allowScroll:NO completion:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
|
||||||
|
|
||||||
|
[self updateLayoutAnimated:YES allowScroll:YES completion:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||||
|
|
||||||
|
[self updateLayoutAnimated:NO allowScroll:NO completion:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark - IBActions
|
||||||
|
|
||||||
|
- (IBAction)targetedUserAction:(UILongPressGestureRecognizer *)sender {
|
||||||
|
|
||||||
|
if (sender.state != UIGestureRecognizerStateBegan)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (self.selectedUser)
|
||||||
|
return;
|
||||||
|
|
||||||
|
MPUserEntity *targetedUser = [self userForAvatar:[self findTargetedAvatar]];
|
||||||
|
if (!targetedUser)
|
||||||
|
return;
|
||||||
|
|
||||||
|
[PearlSheet showSheetWithTitle:targetedUser.name
|
||||||
|
message:nil viewStyle:UIActionSheetStyleBlackTranslucent
|
||||||
|
initSheet:nil
|
||||||
|
tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) {
|
||||||
|
if (buttonIndex == [sheet cancelButtonIndex])
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (buttonIndex == [sheet destructiveButtonIndex]) {
|
||||||
|
[[MPAppDelegate get].managedObjectContextIfReady performBlockAndWait:^{
|
||||||
|
[[MPAppDelegate get].managedObjectContextIfReady deleteObject:targetedUser];
|
||||||
|
}];
|
||||||
|
[[MPAppDelegate get] saveContext];
|
||||||
|
[self updateUsers];
|
||||||
|
} else
|
||||||
|
if (buttonIndex == [sheet firstOtherButtonIndex]) {
|
||||||
|
[[MPAppDelegate get] changeMasterPasswordFor:targetedUser didResetBlock:^{
|
||||||
|
[[self avatarForUser:targetedUser] setSelected:YES];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
[[MPAppDelegate get] saveContext];
|
||||||
|
} cancelTitle:[PearlStrings get].commonButtonCancel destructiveTitle:@"Delete User" otherTitles:@"Reset Password",
|
||||||
|
@"Save",
|
||||||
|
nil];
|
||||||
|
}
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -10,8 +10,13 @@
|
|||||||
|
|
||||||
@interface MPiOSConfig : MPConfig
|
@interface MPiOSConfig : MPConfig
|
||||||
|
|
||||||
|
@property (nonatomic, retain) NSNumber *sendInfo;
|
||||||
@property (nonatomic, retain) NSNumber *helpHidden;
|
@property (nonatomic, retain) NSNumber *helpHidden;
|
||||||
|
@property (nonatomic, retain) NSNumber *userNameHidden;
|
||||||
@property (nonatomic, retain) NSNumber *showQuickStart;
|
@property (nonatomic, retain) NSNumber *showQuickStart;
|
||||||
|
@property (nonatomic, retain) NSNumber *actionsTipShown;
|
||||||
|
@property (nonatomic, retain) NSNumber *typeTipShown;
|
||||||
|
@property (nonatomic, retain) NSNumber *userNameTipShown;
|
||||||
|
|
||||||
+ (MPiOSConfig *)get;
|
+ (MPiOSConfig *)get;
|
||||||
|
|
||||||
|
|||||||
@@ -6,21 +6,24 @@
|
|||||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import "MPiOSConfig.h"
|
|
||||||
|
|
||||||
@implementation MPiOSConfig
|
@implementation MPiOSConfig
|
||||||
@dynamic helpHidden, showQuickStart;
|
@dynamic sendInfo, helpHidden, userNameHidden, showQuickStart, actionsTipShown, typeTipShown, userNameTipShown;
|
||||||
|
|
||||||
- (id)init {
|
- (id)init {
|
||||||
|
|
||||||
if(!(self = [super init]))
|
if (!(self = [super init]))
|
||||||
return self;
|
return self;
|
||||||
|
|
||||||
[self.defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
|
[self.defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(helpHidden)),
|
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(sendInfo)),
|
||||||
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(showQuickStart)),
|
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(helpHidden)),
|
||||||
@"510296984", NSStringFromSelector(@selector(iTunesID)),
|
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(userNameHidden)),
|
||||||
nil]];
|
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(showQuickStart)),
|
||||||
|
@"510296984", NSStringFromSelector(@selector(iTunesID)),
|
||||||
|
PearlBoolNot(self.firstRun), NSStringFromSelector(@selector(actionsTipShown)),
|
||||||
|
PearlBoolNot(self.firstRun), NSStringFromSelector(@selector(typeTipShown)),
|
||||||
|
PearlBool(NO), NSStringFromSelector(@selector(userNameTipShown)),
|
||||||
|
nil]];
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,15 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>© 2011-2012, Lyndir</string>
|
<string>© 2011-2012, Lyndir</string>
|
||||||
|
<key>ReplacementFonts</key>
|
||||||
|
<dict>
|
||||||
|
<key>Futura-CondensedExtraBold</key>
|
||||||
|
<string>Exo-ExtraBold</string>
|
||||||
|
<key>Futura-CondensedMedium</key>
|
||||||
|
<string>Exo-Light</string>
|
||||||
|
<key>Futura-Medium</key>
|
||||||
|
<string>Exo</string>
|
||||||
|
</dict>
|
||||||
<key>UIAppFonts</key>
|
<key>UIAppFonts</key>
|
||||||
<array>
|
<array>
|
||||||
<string>Exo-Black.otf</string>
|
<string>Exo-Black.otf</string>
|
||||||
|
|||||||
@@ -11,12 +11,18 @@
|
|||||||
#import "Pearl-Prefix.pch"
|
#import "Pearl-Prefix.pch"
|
||||||
|
|
||||||
#ifdef __OBJC__
|
#ifdef __OBJC__
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <CoreData/CoreData.h>
|
|
||||||
|
|
||||||
#import "TestFlight.h"
|
#import <UIKit/UIKit.h>
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <CoreData/CoreData.h>
|
||||||
|
|
||||||
|
#import "TestFlight.h"
|
||||||
|
#import <Crashlytics/Crashlytics.h>
|
||||||
|
|
||||||
|
//#define Crashlytics PearlNil
|
||||||
|
//#define TestFlight PearlNil
|
||||||
|
|
||||||
|
#import "MPTypes.h"
|
||||||
|
#import "MPiOSConfig.h"
|
||||||
|
|
||||||
#import "MPTypes.h"
|
|
||||||
#import "MPiOSConfig.h"
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>FooterText</key>
|
<key>FooterText</key>
|
||||||
<string>http://masterpassword.lyndir.com</string>
|
<string>If you're experiencing problems, enabling this will send us details that can help diagnose and resolve them. Great care has been taken to guarantee no private information is ever sent.</string>
|
||||||
<key>Title</key>
|
<key>Title</key>
|
||||||
<string></string>
|
<string></string>
|
||||||
<key>Type</key>
|
<key>Type</key>
|
||||||
@@ -42,9 +42,19 @@
|
|||||||
<key>Key</key>
|
<key>Key</key>
|
||||||
<string>unset</string>
|
<string>unset</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>Type</key>
|
||||||
|
<string>PSToggleSwitchSpecifier</string>
|
||||||
|
<key>Title</key>
|
||||||
|
<string>Send Diagnostic Info</string>
|
||||||
|
<key>Key</key>
|
||||||
|
<string>sendInfo</string>
|
||||||
|
<key>DefaultValue</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>FooterText</key>
|
<key>FooterText</key>
|
||||||
<string>This causes your master password to be remembered while your device is powered on. Similar to your phone's SIM lock, you only need to enter the password once after powering on.</string>
|
<string>When enabled, closing the application will not log out the user. Similar to your phone's SIM lock, you only need to log in once after powering on.</string>
|
||||||
<key>Title</key>
|
<key>Title</key>
|
||||||
<string>Master Password</string>
|
<string>Master Password</string>
|
||||||
<key>Type</key>
|
<key>Type</key>
|
||||||
@@ -54,27 +64,9 @@
|
|||||||
<key>DefaultValue</key>
|
<key>DefaultValue</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>Key</key>
|
<key>Key</key>
|
||||||
<string>rememberKey</string>
|
<string>rememberLogin</string>
|
||||||
<key>Title</key>
|
<key>Title</key>
|
||||||
<string>Remember my password</string>
|
<string>Stay logged in</string>
|
||||||
<key>Type</key>
|
|
||||||
<string>PSToggleSwitchSpecifier</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>FooterText</key>
|
|
||||||
<string>Use this to save your master password in the key chain. This is somewhat less secure should your device get stolen, but it means you won't need to enter the master password anymore. You can compensate for the reduced security by going into General->Passcode Lock, disabling "Simple Passcode", and setting a more secure passcode for your device.</string>
|
|
||||||
<key>Title</key>
|
|
||||||
<string></string>
|
|
||||||
<key>Type</key>
|
|
||||||
<string>PSGroupSpecifier</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>DefaultValue</key>
|
|
||||||
<false/>
|
|
||||||
<key>Key</key>
|
|
||||||
<string>saveKey</string>
|
|
||||||
<key>Title</key>
|
|
||||||
<string>Save my password</string>
|
|
||||||
<key>Type</key>
|
<key>Type</key>
|
||||||
<string>PSToggleSwitchSpecifier</string>
|
<string>PSToggleSwitchSpecifier</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -6,12 +6,10 @@
|
|||||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
#import "MPAppDelegate.h"
|
#import "MPAppDelegate.h"
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[]) {
|
||||||
{
|
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
return UIApplicationMain(argc, argv, nil, NSStringFromClass([MPAppDelegate class]));
|
return UIApplicationMain(argc, argv, nil, NSStringFromClass([MPAppDelegate class]));
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
Resources/AppStore-iOS.png
Normal file
|
After Width: | Height: | Size: 571 KiB |
BIN
Resources/Avatars/avatar-0.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
Resources/Avatars/avatar-0@2x.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
Resources/Avatars/avatar-1.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |