Compare commits
136 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d429044f64 | ||
|
|
b38e8d9ea6 | ||
|
|
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 | ||
|
|
b7e72e98ac | ||
|
|
a0b1e63c74 | ||
|
|
42704ab504 | ||
|
|
6cc74c8898 | ||
|
|
44911f1d9e | ||
|
|
16fc32ee30 | ||
|
|
ac9534c680 | ||
|
|
2020637f0e | ||
|
|
ada6d6b36a | ||
|
|
b0875beb36 | ||
|
|
94818bb242 | ||
|
|
1f68a4cb09 | ||
|
|
f7e64fe4b8 | ||
|
|
301366f1f1 | ||
|
|
941b428cfc | ||
|
|
0d638a4c3f | ||
|
|
cdfafa55cf | ||
|
|
82b8de5e23 | ||
|
|
6bbd183ac9 | ||
|
|
7d9131cdca | ||
|
|
12fb7243d6 | ||
|
|
eb4e25c0ba | ||
|
|
587461144b | ||
|
|
9968491e3b | ||
|
|
db7d68a091 | ||
|
|
c8f9f79bb2 | ||
|
|
21c0565619 | ||
|
|
04bc7a497c | ||
|
|
7c5cea9c8d | ||
|
|
1b90c9bfa3 | ||
|
|
f622b2c7d4 | ||
|
|
98080ceb51 | ||
|
|
f5d9334b06 | ||
|
|
f882d0fb53 | ||
|
|
dfc62fa8a8 | ||
|
|
f5551d4823 | ||
|
|
424479dada | ||
|
|
376953ae56 | ||
|
|
26f8e086bb | ||
|
|
950ce888e2 | ||
|
|
657fef6249 | ||
|
|
0d19202ca7 | ||
|
|
1755bd1607 | ||
|
|
dc8b6aa86c | ||
|
|
0c536ef640 | ||
|
|
fa3680f9c4 | ||
|
|
c8e03ff016 | ||
|
|
3fcac696bb | ||
|
|
d0ae954dbf | ||
|
|
f3c24fd96f | ||
|
|
02ffa9611a | ||
|
|
6bda70920b | ||
|
|
2761355180 | ||
|
|
cbf624b3da | ||
|
|
9899104891 | ||
|
|
d34ec96b94 | ||
|
|
ac735f3ccb | ||
|
|
c508ed8c46 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -20,6 +20,14 @@
|
||||
!/*.xcodeproj/project.xcworkspace/*
|
||||
/*.xcodeproj/project.xcworkspace/xcuserdata
|
||||
|
||||
# Media
|
||||
Press/Background.png
|
||||
Press/Front-Page.png
|
||||
Press/MasterPassword_PressKit/MasterPassword_pressrelease_*.pdf
|
||||
|
||||
# IPA
|
||||
/sendipa/*
|
||||
!/sendipa/sendipa.conf
|
||||
|
||||
# Java
|
||||
MasterPassword/Java/**/target
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -4,3 +4,9 @@
|
||||
[submodule "External/InAppSettingsKit"]
|
||||
path = External/InAppSettingsKit
|
||||
url = git://github.com/futuretap/InAppSettingsKit.git
|
||||
[submodule "External/iCloudStoreManager"]
|
||||
path = External/iCloudStoreManager
|
||||
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
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
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>
|
||||
1
Crashlytics/Crashlytics.framework/Crashlytics
vendored
Symbolic link
1
Crashlytics/Crashlytics.framework/Crashlytics
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
Versions/Current/Crashlytics
|
||||
1
Crashlytics/Crashlytics.framework/Headers
vendored
Symbolic link
1
Crashlytics/Crashlytics.framework/Headers
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
Versions/Current/Headers
|
||||
1
Crashlytics/Crashlytics.framework/Resources
vendored
Symbolic link
1
Crashlytics/Crashlytics.framework/Resources
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
Versions/Current/Resources
|
||||
BIN
Crashlytics/Crashlytics.framework/Versions/A/Crashlytics
vendored
Normal file
BIN
Crashlytics/Crashlytics.framework/Versions/A/Crashlytics
vendored
Normal file
Binary file not shown.
172
Crashlytics/Crashlytics.framework/Versions/A/Headers/Crashlytics.h
vendored
Normal file
172
Crashlytics/Crashlytics.framework/Versions/A/Headers/Crashlytics.h
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
//
|
||||
// Crashlytics.h
|
||||
// Crashlytics
|
||||
//
|
||||
// Copyright 2012 Crashlytics, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
#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;
|
||||
|
||||
@interface Crashlytics : NSObject {
|
||||
@private
|
||||
NSString *_apiKey;
|
||||
NSString *_dataDirectory;
|
||||
NSString *_bundleIdentifier;
|
||||
BOOL _installed;
|
||||
NSMutableDictionary *_customAttributes;
|
||||
id _user;
|
||||
NSInteger _sendButtonIndex;
|
||||
NSInteger _alwaysSendButtonIndex;
|
||||
NSObject <CrashlyticsDelegate> *_delegate;
|
||||
}
|
||||
|
||||
@property (nonatomic, readonly, copy) NSString *apiKey;
|
||||
@property (nonatomic, readonly, copy) NSString *version;
|
||||
@property (nonatomic, assign) BOOL debugMode;
|
||||
|
||||
@property (nonatomic, assign) NSObject <CrashlyticsDelegate> *delegate;
|
||||
|
||||
/**
|
||||
*
|
||||
* The recommended way to install Crashlytics into your application is to place a call
|
||||
* to +startWithAPIKey: in your -application:didFinishLaunchingWithOptions: method.
|
||||
*
|
||||
* This delay defaults to 1 second in order to generally give the application time to
|
||||
* fully finish launching.
|
||||
*
|
||||
**/
|
||||
+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey;
|
||||
+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey afterDelay:(NSTimeInterval)delay;
|
||||
|
||||
/**
|
||||
*
|
||||
* If you need the functionality provided by the CrashlyticsDelegate protocol, you can use
|
||||
* these convenience methods to activate the framework and set the delegate in one call.
|
||||
*
|
||||
**/
|
||||
+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey delegate:(NSObject <CrashlyticsDelegate> *)delegate;
|
||||
+ (Crashlytics *)startWithAPIKey:(NSString *)apiKey delegate:(NSObject <CrashlyticsDelegate> *)delegate afterDelay:(NSTimeInterval)delay;
|
||||
|
||||
/**
|
||||
*
|
||||
* Access the singleton Crashlytics instance.
|
||||
*
|
||||
**/
|
||||
+ (Crashlytics *)sharedInstance;
|
||||
|
||||
/**
|
||||
*
|
||||
* The easiest way to cause a crash - great for testing!
|
||||
*
|
||||
**/
|
||||
- (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
|
||||
|
||||
/**
|
||||
*
|
||||
* The CrashlyticsDelegate protocol provides a mechanism for your application to take
|
||||
* action on events that occur in the Crashlytics crash reporting system. You can make
|
||||
* use of these calls by assigning an object to the Crashlytics' delegate property directly,
|
||||
* or through the convenience startWithAPIKey:delegate:... methods.
|
||||
*
|
||||
**/
|
||||
@protocol CrashlyticsDelegate <NSObject>
|
||||
@optional
|
||||
|
||||
/**
|
||||
*
|
||||
* 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
|
||||
* 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.
|
||||
*
|
||||
**/
|
||||
- (void)crashlyticsDidDetectCrashDuringPreviousExecution:(Crashlytics *)crashlytics;
|
||||
|
||||
@end
|
||||
54
Crashlytics/Crashlytics.framework/Versions/A/Resources/Info.plist
vendored
Normal file
54
Crashlytics/Crashlytics.framework/Versions/A/Resources/Info.plist
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BuildMachineOSBuild</key>
|
||||
<string>11E53</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Crashlytics</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.crashlytics.ios</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Crashlytics</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.1.5</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>iPhoneOS</string>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0101.05.00</string>
|
||||
<key>CrashlyticsAPIKey</key>
|
||||
<string>0d10c90776f5ef5acd01ddbeaca9a6cba4814560</string>
|
||||
<key>DTCompiler</key>
|
||||
<string>com.apple.compilers.llvm.clang.1_0</string>
|
||||
<key>DTPlatformBuild</key>
|
||||
<string>8H7</string>
|
||||
<key>DTPlatformName</key>
|
||||
<string>iphoneos</string>
|
||||
<key>DTPlatformVersion</key>
|
||||
<string>4.3</string>
|
||||
<key>DTSDKBuild</key>
|
||||
<string>8H7</string>
|
||||
<key>DTSDKName</key>
|
||||
<string>iphoneos4.3</string>
|
||||
<key>DTXcode</key>
|
||||
<string>0410</string>
|
||||
<key>DTXcodeBuild</key>
|
||||
<string>4B110</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>3.1</string>
|
||||
<key>UIDeviceFamily</key>
|
||||
<array>
|
||||
<integer>1</integer>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
1
Crashlytics/Crashlytics.framework/Versions/A/Resources/Runner
vendored
Symbolic link
1
Crashlytics/Crashlytics.framework/Versions/A/Resources/Runner
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../run
|
||||
BIN
Crashlytics/Crashlytics.framework/Versions/A/Resources/en.lproj/InfoPlist.strings
vendored
Normal file
BIN
Crashlytics/Crashlytics.framework/Versions/A/Resources/en.lproj/InfoPlist.strings
vendored
Normal file
Binary file not shown.
15
Crashlytics/Crashlytics.framework/Versions/A/Resources/runner.rb
vendored
Normal file
15
Crashlytics/Crashlytics.framework/Versions/A/Resources/runner.rb
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/ruby
|
||||
|
||||
#
|
||||
# WARNING: DO NOT MODIFY THIS FILE.
|
||||
#
|
||||
# Crashlytics
|
||||
# Crashlytics Version: 1.0.0.1
|
||||
#
|
||||
# Copyright Crashlytics, Inc. 2012. All rights reserved.
|
||||
#
|
||||
|
||||
require 'pathname'
|
||||
|
||||
path = Pathname.new(__FILE__).parent
|
||||
`#{path}/../../../run`
|
||||
1
Crashlytics/Crashlytics.framework/Versions/Current
vendored
Symbolic link
1
Crashlytics/Crashlytics.framework/Versions/Current
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
A
|
||||
BIN
Crashlytics/Crashlytics.framework/run
vendored
Executable file
BIN
Crashlytics/Crashlytics.framework/run
vendored
Executable file
Binary file not shown.
8
Crashlytics/Crashlytics.plist
Normal file
8
Crashlytics/Crashlytics.plist
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>API Key</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
Default.png
BIN
Default.png
Binary file not shown.
|
Before Width: | Height: | Size: 49 KiB |
1
External/FontReplacer
vendored
Submodule
1
External/FontReplacer
vendored
Submodule
Submodule External/FontReplacer added at 4e3dea0870
2
External/InAppSettingsKit
vendored
2
External/InAppSettingsKit
vendored
Submodule External/InAppSettingsKit updated: 5fd23fd728...3ae828f48a
2
External/Pearl
vendored
2
External/Pearl
vendored
Submodule External/Pearl updated: bb9d8d10cb...cc97c1be4c
1
External/iCloudStoreManager
vendored
Submodule
1
External/iCloudStoreManager
vendored
Submodule
Submodule External/iCloudStoreManager added at a7b028ff80
10
Localytics/Localytics.plist
Normal file
10
Localytics/Localytics.plist
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Key.development</key>
|
||||
<string>e6238ceba8ec92832e77b1b-9ccd60bc-c39b-11e0-06e4-007f58cb3154</string>
|
||||
<key>Key.distribution</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
57
Localytics/LocalyticsDatabase.h
Normal file
57
Localytics/LocalyticsDatabase.h
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// LocalyticsDatabase.h
|
||||
// LocalyticsDemo
|
||||
//
|
||||
// Created by jkaufman on 5/26/11.
|
||||
// Copyright 2011 Localytics. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <sqlite3.h>
|
||||
|
||||
#define MAX_DATABASE_SIZE 500000 // The maximum allowed disk size of the primary database file at open, in bytes
|
||||
#define VACUUM_THRESHOLD 0.8 // The database is vacuumed after its size exceeds this proportion of the maximum.
|
||||
|
||||
@interface LocalyticsDatabase : NSObject {
|
||||
sqlite3 *_databaseConnection;
|
||||
}
|
||||
|
||||
+ (LocalyticsDatabase *)sharedLocalyticsDatabase;
|
||||
|
||||
- (NSUInteger)databaseSize;
|
||||
- (int)eventCount;
|
||||
- (NSTimeInterval)createdTimestamp;
|
||||
|
||||
- (BOOL)beginTransaction:(NSString *)name;
|
||||
- (BOOL)releaseTransaction:(NSString *)name;
|
||||
- (BOOL)rollbackTransaction:(NSString *)name;
|
||||
|
||||
- (BOOL)incrementLastUploadNumber:(int *)uploadNumber;
|
||||
- (BOOL)incrementLastSessionNumber:(int *)sessionNumber;
|
||||
|
||||
- (BOOL)addEventWithBlobString:(NSString *)blob;
|
||||
- (BOOL)addCloseEventWithBlobString:(NSString *)blob;
|
||||
- (BOOL)addFlowEventWithBlobString:(NSString *)blob;
|
||||
- (BOOL)removeLastCloseAndFlowEvents;
|
||||
|
||||
- (BOOL)addHeaderWithSequenceNumber:(int)number blobString:(NSString *)blob rowId:(sqlite3_int64 *)insertedRowId;
|
||||
- (int)unstagedEventCount;
|
||||
- (BOOL)stageEventsForUpload:(sqlite3_int64)headerId;
|
||||
- (BOOL)updateAppKey:(NSString *)appKey;
|
||||
- (NSString *)uploadBlobString;
|
||||
- (BOOL)deleteUploadedData;
|
||||
- (BOOL)resetAnalyticsData;
|
||||
- (BOOL)vacuumIfRequired;
|
||||
|
||||
- (NSTimeInterval)lastSessionStartTimestamp;
|
||||
- (BOOL)setLastsessionStartTimestamp:(NSTimeInterval)timestamp;
|
||||
|
||||
- (BOOL)isOptedOut;
|
||||
- (BOOL)setOptedOut:(BOOL)optOut;
|
||||
- (NSString *)installId;
|
||||
- (NSString *)appKey; // Most recent app key-- may not be that used to open the session.
|
||||
|
||||
- (NSString *)customDimension:(int)dimension;
|
||||
- (BOOL)setCustomDimension:(int)dimension value:(NSString *)value;
|
||||
|
||||
@end
|
||||
743
Localytics/LocalyticsDatabase.m
Normal file
743
Localytics/LocalyticsDatabase.m
Normal file
@@ -0,0 +1,743 @@
|
||||
//
|
||||
// LocalyticsDatabase.m
|
||||
// LocalyticsDemo
|
||||
//
|
||||
// Created by jkaufman on 5/26/11.
|
||||
// Copyright 2011 Localytics. All rights reserved.
|
||||
//
|
||||
|
||||
#import "LocalyticsDatabase.h"
|
||||
|
||||
#define LOCALYTICS_DIR @".localytics" // Name for the directory in which Localytics database is stored
|
||||
#define LOCALYTICS_DB @"localytics" // File name for the database (without extension)
|
||||
#define BUSY_TIMEOUT 30 // Maximum time SQlite will busy-wait for the database to unlock before returning SQLITE_BUSY
|
||||
|
||||
@interface LocalyticsDatabase ()
|
||||
- (int)schemaVersion;
|
||||
- (void)createSchema;
|
||||
- (void)upgradeToSchemaV2;
|
||||
- (void)upgradeToSchemaV3;
|
||||
- (void)moveDbToCaches;
|
||||
- (NSString *)randomUUID;
|
||||
@end
|
||||
|
||||
@implementation LocalyticsDatabase
|
||||
|
||||
// The singleton database object.
|
||||
static LocalyticsDatabase *_sharedLocalyticsDatabase = nil;
|
||||
|
||||
+ (NSString *)localyticsDirectoryPath {
|
||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
return [[paths objectAtIndex:0] stringByAppendingPathComponent:LOCALYTICS_DIR];
|
||||
}
|
||||
|
||||
+ (NSString *)localyticsDatabasePath {
|
||||
NSString *path = [[LocalyticsDatabase localyticsDirectoryPath] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.sqlite", LOCALYTICS_DB]];
|
||||
return path;
|
||||
}
|
||||
|
||||
#pragma mark Singleton Class
|
||||
+ (LocalyticsDatabase *)sharedLocalyticsDatabase {
|
||||
@synchronized(self) {
|
||||
if (_sharedLocalyticsDatabase == nil) {
|
||||
_sharedLocalyticsDatabase = [[self alloc] init];
|
||||
}
|
||||
}
|
||||
return _sharedLocalyticsDatabase;
|
||||
}
|
||||
|
||||
- (LocalyticsDatabase *)init {
|
||||
if((self = [super init])) {
|
||||
|
||||
// Mover any data that a previous library may have left in the documents directory
|
||||
[self moveDbToCaches];
|
||||
|
||||
// Create directory structure for Localytics.
|
||||
NSString *directoryPath = [LocalyticsDatabase localyticsDirectoryPath];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:directoryPath]) {
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
}
|
||||
|
||||
// Attempt to open database. It will be created if it does not exist, already.
|
||||
NSString *dbPath = [LocalyticsDatabase localyticsDatabasePath];
|
||||
int code = sqlite3_open([dbPath UTF8String], &_databaseConnection);
|
||||
|
||||
// If we were unable to open the database, it is likely corrupted. Clobber it and move on.
|
||||
if (code != SQLITE_OK) {
|
||||
[[NSFileManager defaultManager] removeItemAtPath:dbPath error:nil];
|
||||
code = sqlite3_open([dbPath UTF8String], &_databaseConnection);
|
||||
}
|
||||
|
||||
// Check db connection, creating schema if necessary.
|
||||
if (code == SQLITE_OK) {
|
||||
sqlite3_busy_timeout(_databaseConnection, BUSY_TIMEOUT); // Defaults to 0, otherwise.
|
||||
if ([self schemaVersion] == 0) {
|
||||
[self createSchema];
|
||||
}
|
||||
}
|
||||
|
||||
// Perform any Migrations if necessary
|
||||
if ([self schemaVersion] < 2) {
|
||||
[self upgradeToSchemaV2];
|
||||
}
|
||||
if ([self schemaVersion] < 3) {
|
||||
[self upgradeToSchemaV3];
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Database
|
||||
|
||||
- (BOOL)beginTransaction:(NSString *)name {
|
||||
const char *sql = [[NSString stringWithFormat:@"SAVEPOINT %@", name] cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
int code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL);
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)releaseTransaction:(NSString *)name {
|
||||
const char *sql = [[NSString stringWithFormat:@"RELEASE SAVEPOINT %@", name] cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
int code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL);
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)rollbackTransaction:(NSString *)name {
|
||||
const char *sql = [[NSString stringWithFormat:@"ROLLBACK SAVEPOINT %@", name] cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
int code = sqlite3_exec(_databaseConnection, sql, NULL, NULL, NULL);
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (int)schemaVersion {
|
||||
int version = 0;
|
||||
const char *sql = "SELECT MAX(schema_version) FROM localytics_info";
|
||||
sqlite3_stmt *selectSchemaVersion;
|
||||
if(sqlite3_prepare_v2(_databaseConnection, sql, -1, &selectSchemaVersion, NULL) == SQLITE_OK) {
|
||||
if(sqlite3_step(selectSchemaVersion) == SQLITE_ROW) {
|
||||
version = sqlite3_column_int(selectSchemaVersion, 0);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(selectSchemaVersion);
|
||||
return version;
|
||||
}
|
||||
|
||||
- (NSString *)installId {
|
||||
NSString *installId = nil;
|
||||
|
||||
sqlite3_stmt *selectInstallId;
|
||||
sqlite3_prepare_v2(_databaseConnection, "SELECT install_id FROM localytics_info", -1, &selectInstallId, NULL);
|
||||
int code = sqlite3_step(selectInstallId);
|
||||
if (code == SQLITE_ROW && sqlite3_column_text(selectInstallId, 0)) {
|
||||
installId = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectInstallId, 0)];
|
||||
}
|
||||
sqlite3_finalize(selectInstallId);
|
||||
|
||||
return installId;
|
||||
}
|
||||
|
||||
- (NSString *)appKey {
|
||||
NSString *appKey = nil;
|
||||
|
||||
sqlite3_stmt *selectAppKey;
|
||||
sqlite3_prepare_v2(_databaseConnection, "SELECT app_key FROM localytics_info", -1, &selectAppKey, NULL);
|
||||
int code = sqlite3_step(selectAppKey);
|
||||
if (code == SQLITE_ROW && sqlite3_column_text(selectAppKey, 0)) {
|
||||
appKey = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectAppKey, 0)];
|
||||
}
|
||||
sqlite3_finalize(selectAppKey);
|
||||
|
||||
return appKey;
|
||||
}
|
||||
|
||||
// Due to the new iOS storage guidelines it is necessary to move the database out of the documents directory
|
||||
// and into the /library/caches directory
|
||||
- (void)moveDbToCaches {
|
||||
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||
NSString *localyticsDocumentsDirectory = [[documentPaths objectAtIndex:0] stringByAppendingPathComponent:LOCALYTICS_DIR];
|
||||
NSArray *cachesPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
NSString *localyticsCachesDirectory = [[cachesPaths objectAtIndex:0] stringByAppendingPathComponent:LOCALYTICS_DIR];
|
||||
|
||||
// If the old directory doesn't exist, there is nothing else to do here
|
||||
if([[NSFileManager defaultManager] fileExistsAtPath:localyticsDocumentsDirectory] == NO)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to move the directory
|
||||
if(NO == [[NSFileManager defaultManager] moveItemAtPath:localyticsDocumentsDirectory
|
||||
toPath:localyticsCachesDirectory
|
||||
error:nil])
|
||||
{
|
||||
// If the move failed try and, delete the old directory
|
||||
[ [NSFileManager defaultManager] removeItemAtPath:localyticsDocumentsDirectory error:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)createSchema {
|
||||
int code = SQLITE_OK;
|
||||
|
||||
// Execute schema creation within a single transaction.
|
||||
code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL);
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"CREATE TABLE upload_headers ("
|
||||
"sequence_number INTEGER PRIMARY KEY, "
|
||||
"blob_string TEXT)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"CREATE TABLE events ("
|
||||
"event_id INTEGER PRIMARY KEY AUTOINCREMENT, " // In case foreign key constraints are reintroduced.
|
||||
"upload_header INTEGER, "
|
||||
"blob_string TEXT NOT NULL)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"CREATE TABLE localytics_info ("
|
||||
"schema_version INTEGER PRIMARY KEY, "
|
||||
"last_upload_number INTEGER, "
|
||||
"last_session_number INTEGER, "
|
||||
"opt_out BOOLEAN, "
|
||||
"last_close_event INTEGER, "
|
||||
"last_flow_event INTEGER, "
|
||||
"last_session_start REAL, "
|
||||
"app_key CHAR(64), "
|
||||
"custom_d0 CHAR(64), "
|
||||
"custom_d1 CHAR(64), "
|
||||
"custom_d2 CHAR(64), "
|
||||
"custom_d3 CHAR(64) "
|
||||
")",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"INSERT INTO localytics_info (schema_version, last_upload_number, last_session_number, opt_out) "
|
||||
"VALUES (3, 0, 0, 0)", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
// Commit transaction.
|
||||
if (code == SQLITE_OK || code == SQLITE_DONE) {
|
||||
sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL);
|
||||
} else {
|
||||
sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// V2 adds a unique identifier for each installation
|
||||
// This identifier has been moved to user preferences so the database an live in the caches directory
|
||||
// Also adds storage for custom dimensions
|
||||
- (void)upgradeToSchemaV2 {
|
||||
int code = SQLITE_OK;
|
||||
|
||||
code = sqlite3_exec(_databaseConnection, "BEGIN", NULL, NULL, NULL);
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD install_id CHAR(40)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD custom_d0 CHAR(64)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD custom_d1 CHAR(64)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD custom_d2 CHAR(64)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD custom_d3 CHAR(64)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
// Attempt to set schema version and install_id regardless of the result code following the ALTER statements above.
|
||||
// This is necessary because a previous version of the library performed the migration without setting these values.
|
||||
// The transaction will succeed even if the individual statements fail with errors (eg. "duplicate column name").
|
||||
sqlite3_stmt *updateLocalyticsInfo;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info set install_id = ?, schema_version = 2 ", -1, &updateLocalyticsInfo, NULL);
|
||||
sqlite3_bind_text (updateLocalyticsInfo, 1, [[self randomUUID] UTF8String], -1, SQLITE_TRANSIENT);
|
||||
code = sqlite3_step(updateLocalyticsInfo);
|
||||
sqlite3_finalize(updateLocalyticsInfo);
|
||||
|
||||
// Commit transaction.
|
||||
if (code == SQLITE_OK || code == SQLITE_DONE) {
|
||||
sqlite3_exec(_databaseConnection, "COMMIT", NULL, NULL, NULL);
|
||||
} else {
|
||||
sqlite3_exec(_databaseConnection, "ROLLBACK", NULL, NULL, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
// V3 adds a field for the last app key and patches a V2 migration issue.
|
||||
- (void)upgradeToSchemaV3 {
|
||||
sqlite3_exec(_databaseConnection,
|
||||
"ALTER TABLE localytics_info ADD app_key CHAR(64)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
- (NSUInteger)databaseSize {
|
||||
NSUInteger size = 0;
|
||||
NSDictionary *fileAttributes = [[NSFileManager defaultManager]
|
||||
attributesOfItemAtPath:[LocalyticsDatabase localyticsDatabasePath]
|
||||
error:nil];
|
||||
size = [fileAttributes fileSize];
|
||||
return size;
|
||||
}
|
||||
|
||||
- (int) eventCount {
|
||||
int count = 0;
|
||||
const char *sql = "SELECT count(*) FROM events";
|
||||
sqlite3_stmt *selectEventCount;
|
||||
|
||||
if(sqlite3_prepare_v2(_databaseConnection, sql, -1, &selectEventCount, NULL) == SQLITE_OK)
|
||||
{
|
||||
if(sqlite3_step(selectEventCount) == SQLITE_ROW) {
|
||||
count = sqlite3_column_int(selectEventCount, 0);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(selectEventCount);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
- (NSTimeInterval)createdTimestamp {
|
||||
NSTimeInterval timestamp = 0;
|
||||
NSDictionary *fileAttributes = [[NSFileManager defaultManager]
|
||||
attributesOfItemAtPath:[LocalyticsDatabase localyticsDatabasePath]
|
||||
error:nil];
|
||||
timestamp = [[fileAttributes fileCreationDate] timeIntervalSince1970];
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
- (NSTimeInterval)lastSessionStartTimestamp {
|
||||
|
||||
NSTimeInterval lastSessionStart = 0;
|
||||
|
||||
sqlite3_stmt *selectLastSessionStart;
|
||||
sqlite3_prepare_v2(_databaseConnection, "SELECT last_session_start FROM localytics_info", -1, &selectLastSessionStart, NULL);
|
||||
int code = sqlite3_step(selectLastSessionStart);
|
||||
if (code == SQLITE_ROW) {
|
||||
lastSessionStart = sqlite3_column_double(selectLastSessionStart, 0) == 1;
|
||||
}
|
||||
sqlite3_finalize(selectLastSessionStart);
|
||||
|
||||
return lastSessionStart;
|
||||
}
|
||||
|
||||
- (BOOL)setLastsessionStartTimestamp:(NSTimeInterval)timestamp {
|
||||
sqlite3_stmt *updateLastSessionStart;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET last_session_start = ?", -1, &updateLastSessionStart, NULL);
|
||||
sqlite3_bind_double(updateLastSessionStart, 1, timestamp);
|
||||
int code = sqlite3_step(updateLastSessionStart);
|
||||
sqlite3_finalize(updateLastSessionStart);
|
||||
|
||||
return code == SQLITE_DONE;
|
||||
}
|
||||
|
||||
- (BOOL)isOptedOut {
|
||||
BOOL optedOut = NO;
|
||||
|
||||
sqlite3_stmt *selectOptOut;
|
||||
sqlite3_prepare_v2(_databaseConnection, "SELECT opt_out FROM localytics_info", -1, &selectOptOut, NULL);
|
||||
int code = sqlite3_step(selectOptOut);
|
||||
if (code == SQLITE_ROW) {
|
||||
optedOut = sqlite3_column_int(selectOptOut, 0) == 1;
|
||||
}
|
||||
sqlite3_finalize(selectOptOut);
|
||||
|
||||
return optedOut;
|
||||
}
|
||||
|
||||
- (BOOL)setOptedOut:(BOOL)optOut {
|
||||
sqlite3_stmt *updateOptedOut;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET opt_out = ?", -1, &updateOptedOut, NULL);
|
||||
sqlite3_bind_int(updateOptedOut, 1, optOut);
|
||||
int code = sqlite3_step(updateOptedOut);
|
||||
sqlite3_finalize(updateOptedOut);
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (NSString *)customDimension:(int)dimension {
|
||||
if(dimension < 0 || dimension > 3) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *value = nil;
|
||||
NSString *query = [NSString stringWithFormat:@"select custom_d%i from localytics_info", dimension];
|
||||
|
||||
sqlite3_stmt *selectCustomDim;
|
||||
sqlite3_prepare_v2(_databaseConnection, [query UTF8String], -1, &selectCustomDim, NULL);
|
||||
int code = sqlite3_step(selectCustomDim);
|
||||
if (code == SQLITE_ROW && sqlite3_column_text(selectCustomDim, 0)) {
|
||||
value = [NSString stringWithUTF8String:(char *)sqlite3_column_text(selectCustomDim, 0)];
|
||||
}
|
||||
sqlite3_finalize(selectCustomDim);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
- (BOOL)setCustomDimension:(int)dimension value:(NSString *)value {
|
||||
if(dimension < 0 || dimension > 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NSString *query = [NSString stringWithFormat:@"update localytics_info SET custom_d%i = %@",
|
||||
dimension,
|
||||
(value == nil) ? @"null" : [NSString stringWithFormat:@"\"%@\"", value]];
|
||||
|
||||
int code = sqlite3_exec(_databaseConnection, [query UTF8String], NULL, NULL, NULL);
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)incrementLastUploadNumber:(int *)uploadNumber {
|
||||
NSString *t = @"increment_upload_number";
|
||||
int code = SQLITE_OK;
|
||||
|
||||
code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR;
|
||||
|
||||
if(code == SQLITE_OK) {
|
||||
// Increment value
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"UPDATE localytics_info "
|
||||
"SET last_upload_number = (last_upload_number + 1)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if(code == SQLITE_OK) {
|
||||
// Retrieve new value
|
||||
sqlite3_stmt *selectUploadNumber;
|
||||
sqlite3_prepare_v2(_databaseConnection,
|
||||
"SELECT last_upload_number FROM localytics_info",
|
||||
-1, &selectUploadNumber, NULL);
|
||||
code = sqlite3_step(selectUploadNumber);
|
||||
if (code == SQLITE_ROW) {
|
||||
*uploadNumber = sqlite3_column_int(selectUploadNumber, 0);
|
||||
}
|
||||
sqlite3_finalize(selectUploadNumber);
|
||||
}
|
||||
|
||||
if(code == SQLITE_ROW) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
|
||||
return code == SQLITE_ROW;
|
||||
}
|
||||
|
||||
- (BOOL)incrementLastSessionNumber:(int *)sessionNumber {
|
||||
NSString *t = @"increment_session_number";
|
||||
int code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR;
|
||||
|
||||
if(code == SQLITE_OK) {
|
||||
// Increment value
|
||||
code = sqlite3_exec(_databaseConnection,
|
||||
"UPDATE localytics_info "
|
||||
"SET last_session_number = (last_session_number + 1)",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if(code == SQLITE_OK) {
|
||||
// Retrieve new value
|
||||
sqlite3_stmt *selectSessionNumber;
|
||||
sqlite3_prepare_v2(_databaseConnection,
|
||||
"SELECT last_session_number FROM localytics_info",
|
||||
-1, &selectSessionNumber, NULL);
|
||||
code = sqlite3_step(selectSessionNumber);
|
||||
if (code == SQLITE_ROW && sessionNumber != NULL) {
|
||||
*sessionNumber = sqlite3_column_int(selectSessionNumber, 0);
|
||||
}
|
||||
sqlite3_finalize(selectSessionNumber);
|
||||
}
|
||||
|
||||
if(code == SQLITE_ROW) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
|
||||
return code == SQLITE_ROW;
|
||||
}
|
||||
|
||||
- (BOOL)addEventWithBlobString:(NSString *)blob {
|
||||
|
||||
int code = SQLITE_OK;
|
||||
sqlite3_stmt *insertEvent;
|
||||
sqlite3_prepare_v2(_databaseConnection, "INSERT INTO events (blob_string) VALUES (?)", -1, &insertEvent, NULL);
|
||||
sqlite3_bind_text(insertEvent, 1, [blob UTF8String], -1, SQLITE_TRANSIENT);
|
||||
code = sqlite3_step(insertEvent);
|
||||
sqlite3_finalize(insertEvent);
|
||||
|
||||
return code == SQLITE_DONE;
|
||||
}
|
||||
|
||||
- (BOOL)addCloseEventWithBlobString:(NSString *)blob {
|
||||
NSString *t = @"add_close_event";
|
||||
BOOL success = [self beginTransaction:t];
|
||||
|
||||
// Add close event.
|
||||
if (success) {
|
||||
success = [self addEventWithBlobString:blob];
|
||||
}
|
||||
|
||||
// Record row id to localytics_info so that it can be removed if the session resumes.
|
||||
if (success) {
|
||||
sqlite3_stmt *updateCloseEvent;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET last_close_event = (SELECT event_id FROM events WHERE rowid = ?)", -1, &updateCloseEvent, NULL);
|
||||
sqlite3_int64 lastRow = sqlite3_last_insert_rowid(_databaseConnection);
|
||||
sqlite3_bind_int64(updateCloseEvent, 1, lastRow);
|
||||
int code = sqlite3_step(updateCloseEvent);
|
||||
sqlite3_finalize(updateCloseEvent);
|
||||
success = code == SQLITE_DONE;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)addFlowEventWithBlobString:(NSString *)blob {
|
||||
NSString *t = @"add_flow_event";
|
||||
BOOL success = [self beginTransaction:t];
|
||||
|
||||
// Add flow event.
|
||||
if (success) {
|
||||
success = [self addEventWithBlobString:blob];
|
||||
}
|
||||
|
||||
// Record row id to localytics_info so that it can be removed if the session resumes.
|
||||
if (success) {
|
||||
sqlite3_stmt *updateFlowEvent;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info SET last_flow_event = (SELECT event_id FROM events WHERE rowid = ?)", -1, &updateFlowEvent, NULL);
|
||||
sqlite3_int64 lastRow = sqlite3_last_insert_rowid(_databaseConnection);
|
||||
sqlite3_bind_int64(updateFlowEvent, 1, lastRow);
|
||||
int code = sqlite3_step(updateFlowEvent);
|
||||
sqlite3_finalize(updateFlowEvent);
|
||||
success = code == SQLITE_DONE;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)removeLastCloseAndFlowEvents {
|
||||
// Attempt to remove the last recorded close event.
|
||||
// Fail quietly if none was saved or it was previously removed.
|
||||
int code = sqlite3_exec(_databaseConnection, "DELETE FROM events WHERE event_id = (SELECT last_close_event FROM localytics_info) OR event_id = (SELECT last_flow_event FROM localytics_info)", NULL, NULL, NULL);
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)addHeaderWithSequenceNumber:(int)number blobString:(NSString *)blob rowId:(sqlite3_int64 *)insertedRowId {
|
||||
sqlite3_stmt *insertHeader;
|
||||
sqlite3_prepare_v2(_databaseConnection, "INSERT INTO upload_headers (sequence_number, blob_string) VALUES (?, ?)", -1, &insertHeader, NULL);
|
||||
sqlite3_bind_int(insertHeader, 1, number);
|
||||
sqlite3_bind_text(insertHeader, 2, [blob UTF8String], -1, SQLITE_TRANSIENT);
|
||||
int code = sqlite3_step(insertHeader);
|
||||
sqlite3_finalize(insertHeader);
|
||||
|
||||
if (code == SQLITE_DONE && insertedRowId != NULL) {
|
||||
*insertedRowId = sqlite3_last_insert_rowid(_databaseConnection);
|
||||
}
|
||||
|
||||
return code == SQLITE_DONE;
|
||||
}
|
||||
|
||||
- (int)unstagedEventCount {
|
||||
int rowCount = 0;
|
||||
sqlite3_stmt *selectEventCount;
|
||||
sqlite3_prepare_v2(_databaseConnection, "SELECT COUNT(*) FROM events WHERE UPLOAD_HEADER IS NULL", -1, &selectEventCount, NULL);
|
||||
int code = sqlite3_step(selectEventCount);
|
||||
if (code == SQLITE_ROW) {
|
||||
rowCount = sqlite3_column_int(selectEventCount, 0);
|
||||
}
|
||||
sqlite3_finalize(selectEventCount);
|
||||
|
||||
return rowCount;
|
||||
}
|
||||
|
||||
- (BOOL)stageEventsForUpload:(sqlite3_int64)headerId {
|
||||
|
||||
// Associate all outstanding events with the given upload header ID.
|
||||
NSString *stageEvents = [NSString stringWithFormat:@"UPDATE events SET upload_header = ? WHERE upload_header IS NULL"];
|
||||
sqlite3_stmt *updateEvents;
|
||||
sqlite3_prepare_v2(_databaseConnection, [stageEvents UTF8String], -1, &updateEvents, NULL);
|
||||
sqlite3_bind_int(updateEvents, 1, headerId);
|
||||
int code = sqlite3_step(updateEvents);
|
||||
sqlite3_finalize(updateEvents);
|
||||
BOOL success = (code == SQLITE_DONE);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)updateAppKey:(NSString *)appKey {
|
||||
sqlite3_stmt *updateAppKey;
|
||||
sqlite3_prepare_v2(_databaseConnection, "UPDATE localytics_info set app_key = ?", -1, &updateAppKey, NULL);
|
||||
sqlite3_bind_text (updateAppKey, 1, [appKey UTF8String], -1, SQLITE_TRANSIENT);
|
||||
int code = sqlite3_step(updateAppKey);
|
||||
sqlite3_finalize(updateAppKey);
|
||||
BOOL success = (code == SQLITE_DONE);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
- (NSString *)uploadBlobString {
|
||||
|
||||
// Retrieve the blob strings of each upload header and its child events, in order.
|
||||
const char *sql = "SELECT * FROM ( "
|
||||
" SELECT h.blob_string AS 'blob', h.sequence_number as 'seq', 0 FROM upload_headers h"
|
||||
" UNION ALL "
|
||||
" SELECT e.blob_string AS 'blob', e.upload_header as 'seq', 1 FROM events e"
|
||||
") "
|
||||
"ORDER BY 2, 3";
|
||||
sqlite3_stmt *selectBlobs;
|
||||
sqlite3_prepare_v2(_databaseConnection, sql, -1, &selectBlobs, NULL);
|
||||
NSMutableString *uploadBlobString = [NSMutableString string];
|
||||
while (sqlite3_step(selectBlobs) == SQLITE_ROW) {
|
||||
const char *blob = (const char *)sqlite3_column_text(selectBlobs, 0);
|
||||
if (blob != NULL) {
|
||||
NSString *blobString = [[NSString alloc] initWithCString:blob encoding:NSUTF8StringEncoding];
|
||||
[uploadBlobString appendString:blobString];
|
||||
[blobString release];
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(selectBlobs);
|
||||
|
||||
return [[uploadBlobString copy] autorelease];
|
||||
}
|
||||
|
||||
- (BOOL)deleteUploadedData {
|
||||
// Delete all headers and staged events.
|
||||
NSString *t = @"delete_upload_data";
|
||||
int code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR;
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection, "DELETE FROM events WHERE upload_header IS NOT NULL", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection, "DELETE FROM upload_headers", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)resetAnalyticsData {
|
||||
// Delete or zero all analytics data.
|
||||
// Reset: headers, events, session number, upload number, last session start, last close event, and last flow event.
|
||||
// Unaffected: schema version, opt out status, install ID (deprecated), and app key.
|
||||
|
||||
NSString *t = @"reset_analytics_data";
|
||||
int code = [self beginTransaction:t] ? SQLITE_OK : SQLITE_ERROR;
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection, "DELETE FROM events", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection, "DELETE FROM upload_headers", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
code = sqlite3_exec(_databaseConnection,"UPDATE localytics_info SET last_session_number = 0, last_upload_number = 0,"
|
||||
"last_close_event = null, last_flow_event = null, last_session_start = null, "
|
||||
"custom_d0 = null, custom_d1 = null, custom_d2 = null, custom_d3 = null",
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
if (code == SQLITE_OK) {
|
||||
[self releaseTransaction:t];
|
||||
} else {
|
||||
[self rollbackTransaction:t];
|
||||
}
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (BOOL)vacuumIfRequired {
|
||||
int code = SQLITE_OK;
|
||||
if ([self databaseSize] > MAX_DATABASE_SIZE * VACUUM_THRESHOLD) {
|
||||
code = sqlite3_exec(_databaseConnection, "VACUUM", NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
return code == SQLITE_OK;
|
||||
}
|
||||
|
||||
- (NSString *)randomUUID {
|
||||
CFUUIDRef theUUID = CFUUIDCreate(NULL);
|
||||
CFStringRef stringUUID = CFUUIDCreateString(NULL, theUUID);
|
||||
CFRelease(theUUID);
|
||||
return [(NSString *)stringUUID autorelease];
|
||||
}
|
||||
|
||||
#pragma mark - Lifecycle
|
||||
|
||||
+ (id)allocWithZone:(NSZone *)zone {
|
||||
@synchronized(self) {
|
||||
if (_sharedLocalyticsDatabase == nil) {
|
||||
_sharedLocalyticsDatabase = [super allocWithZone:zone];
|
||||
return _sharedLocalyticsDatabase;
|
||||
}
|
||||
}
|
||||
// returns nil on subsequent allocations
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)retain {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (unsigned)retainCount {
|
||||
// maximum value of an unsigned int - prevents additional retains for the class
|
||||
return UINT_MAX;
|
||||
}
|
||||
|
||||
- (oneway void)release {
|
||||
// ignore release commands
|
||||
}
|
||||
|
||||
- (id)autorelease {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
sqlite3_close(_databaseConnection);
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
216
Localytics/LocalyticsSession.h
Normal file
216
Localytics/LocalyticsSession.h
Normal file
@@ -0,0 +1,216 @@
|
||||
// LocalyticsSession.h
|
||||
// Copyright (C) 2009 Char Software Inc., DBA Localytics
|
||||
//
|
||||
// This code is provided under the Localytics Modified BSD License.
|
||||
// A copy of this license has been distributed in a file called LICENSE
|
||||
// with this source code.
|
||||
//
|
||||
// Please visit www.localytics.com for more information.
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
// Set this to true to enable localytics traces (useful for debugging)
|
||||
#define DO_LOCALYTICS_LOGGING false
|
||||
|
||||
/*!
|
||||
@class LocalyticsSession
|
||||
@discussion The class which manages creating, collecting, & uploading a Localytics session.
|
||||
Please see the following guides for information on how to best use this
|
||||
library, sample code, and other useful information:
|
||||
<ul>
|
||||
<li><a href="http://wiki.localytics.com/index.php?title=Developer's_Integration_Guide">Main Developer's Integration Guide</a></li>
|
||||
</ul>
|
||||
|
||||
<strong>Best Practices</strong>
|
||||
<ul>
|
||||
<li>Instantiate the LocalyticsSession object in applicationDidFinishLaunching.</li>
|
||||
<li>Open your session and begin your uploads in applicationDidFinishLaunching. This way the
|
||||
upload has time to complete and it all happens before your users have a
|
||||
chance to begin any data intensive actions of their own.</li>
|
||||
<li>Close the session in applicationWillTerminate, and in applicationDidEnterBackground.</li>
|
||||
<li>Resume the session in applicationWillEnterForeground.</li>
|
||||
<li>Do not call any Localytics functions inside a loop. Instead, calls
|
||||
such as <code>tagEvent</code> should follow user actions. This limits the
|
||||
amount of data which is stored and uploaded.</li>
|
||||
<li>Do not use multiple LocalticsSession objects to upload data with
|
||||
multiple application keys. This can cause invalid state.</li>
|
||||
</ul>
|
||||
|
||||
@author Localytics
|
||||
*/
|
||||
@interface LocalyticsSession : NSObject {
|
||||
|
||||
BOOL _hasInitialized; // Whether or not the session object has been initialized.
|
||||
BOOL _isSessionOpen; // Whether or not this session has been opened.
|
||||
float _backgroundSessionTimeout; // If an App stays in the background for more
|
||||
// than this many seconds, start a new session
|
||||
// when it returns to foreground.
|
||||
@private
|
||||
#pragma mark Member Variables
|
||||
dispatch_queue_t _queue; // Queue of Localytics block objects.
|
||||
dispatch_group_t _criticalGroup; // Group of blocks the must complete before backgrounding.
|
||||
NSString *_sessionUUID; // Unique identifier for this session.
|
||||
NSString *_applicationKey; // Unique identifier for the instrumented application
|
||||
NSTimeInterval _lastSessionStartTimestamp; // The start time of the most recent session.
|
||||
NSDate *_sessionResumeTime; // Time session was started or resumed.
|
||||
NSDate *_sessionCloseTime; // Time session was closed.
|
||||
NSMutableString *_unstagedFlowEvents; // Comma-delimited list of app screens and events tagged during this
|
||||
// session that have NOT been staged for upload.
|
||||
NSMutableString *_stagedFlowEvents; // App screens and events tagged during this session that HAVE been staged
|
||||
// for upload.
|
||||
NSMutableString *_screens; // Comma-delimited list of screens tagged during this session.
|
||||
NSTimeInterval _sessionActiveDuration; // Duration that session open.
|
||||
BOOL _sessionHasBeenOpen; // Whether or not this session has ever been open.
|
||||
}
|
||||
|
||||
@property dispatch_queue_t queue;
|
||||
@property dispatch_group_t criticalGroup;
|
||||
@property BOOL isSessionOpen;
|
||||
@property BOOL hasInitialized;
|
||||
@property float backgroundSessionTimeout;
|
||||
|
||||
#pragma mark Public Methods
|
||||
/*!
|
||||
@method sharedLocalyticsSession
|
||||
@abstract Accesses the Session object. This is a Singleton class which maintains
|
||||
a single session throughout your application. It is possible to manage your own
|
||||
session, but this is the easiest way to access the Localytics object throughout your code.
|
||||
The class is accessed within the code using the following syntax:
|
||||
[[LocalyticsSession sharedLocalyticsSession] functionHere]
|
||||
So, to tag an event, all that is necessary, anywhere in the code is:
|
||||
[[LocalyticsSession sharedLocalyticsSession] tagEvent:@"MY_EVENT"];
|
||||
*/
|
||||
+ (LocalyticsSession *)sharedLocalyticsSession;
|
||||
|
||||
/*!
|
||||
@method LocalyticsSession
|
||||
@abstract Initializes the Localytics Object. Not necessary if you choose to use startSession.
|
||||
@param applicationKey The key unique for each application generated at www.localytics.com
|
||||
*/
|
||||
- (void)LocalyticsSession:(NSString *)appKey;
|
||||
|
||||
/*!
|
||||
@method startSession
|
||||
@abstract An optional convenience initialize method that also calls the LocalyticsSession, open &
|
||||
upload methods. Best Practice is to call open & upload immediately after Localytics Session when loading an app,
|
||||
this method fascilitates that behavior.
|
||||
It is recommended that this call be placed in <code>applicationDidFinishLaunching</code>.
|
||||
@param applicationKey The key unique for each application generated
|
||||
at www.localytics.com
|
||||
*/
|
||||
- (void)startSession:(NSString *)appKey;
|
||||
|
||||
/*!
|
||||
@method setOptIn
|
||||
@abstract (OPTIONAL) Allows the application to control whether or not it will collect user data.
|
||||
Even if this call is used, it is necessary to continue calling upload(). No new data will be
|
||||
collected, so nothing new will be uploaded but it is necessary to upload an event telling the
|
||||
server this user has opted out.
|
||||
@param optedIn True if the user is opted in, false otherwise.
|
||||
*/
|
||||
- (void)setOptIn:(BOOL)optedIn;
|
||||
|
||||
/*!
|
||||
@method isOptedIn
|
||||
@abstract (OPTIONAL) Whether or not this user has is opted in or out. The only way they can be
|
||||
opted out is if setOptIn(false) has been called before this. This function should only be
|
||||
used to pre-populate a checkbox in an options menu. It is not recommended that an application
|
||||
branch based on Localytics instrumentation because this creates an additional test case. If
|
||||
the app is opted out, all subsequent Localytics calls will return immediately.
|
||||
@result true if the user is opted in, false otherwise.
|
||||
*/
|
||||
- (BOOL)isOptedIn;
|
||||
|
||||
/*!
|
||||
@method open
|
||||
@abstract Opens the Localytics session. Not necessary if you choose to use startSession.
|
||||
The session time as presented on the website is the time between <code>open</code> and the
|
||||
final <code>close</code> so it is recommended to open the session as early as possible, and close
|
||||
it at the last moment. The session must be opened before any tags can
|
||||
be written. It is recommended that this call be placed in <code>applicationDidFinishLaunching</code>.
|
||||
<br>
|
||||
If for any reason this is called more than once every subsequent open call
|
||||
will be ignored.
|
||||
*/
|
||||
- (void)open;
|
||||
|
||||
/*!
|
||||
@method resume
|
||||
@abstract Resumes the Localytics session. When the App enters the background, the session is
|
||||
closed and the time of closing is recorded. When the app returns to the foreground, the session
|
||||
is resumed. If the time since closing is greater than BACKGROUND_SESSION_TIMEOUT, (15 seconds
|
||||
by default) a new session is created, and uploading is triggered. Otherwise, the previous session
|
||||
is reopened.
|
||||
*/
|
||||
- (void)resume;
|
||||
|
||||
/*!
|
||||
@method close
|
||||
@abstract Closes the Localytics session. This should be called in
|
||||
<code>applicationWillTerminate</code>.
|
||||
<br>
|
||||
If close is not called, the session will still be uploaded but no
|
||||
events will be processed and the session time will not appear. This is
|
||||
because the session is not yet closed so it should not be used in
|
||||
comparison with sessions which are closed.
|
||||
*/
|
||||
- (void)close;
|
||||
|
||||
/*!
|
||||
@method tagEvent
|
||||
@abstract Allows a session to tag a particular event as having occurred. For
|
||||
example, if a view has three buttons, it might make sense to tag
|
||||
each button click with the name of the button which was clicked.
|
||||
For another example, in a game with many levels it might be valuable
|
||||
to create a new tag every time the user gets to a new level in order
|
||||
to determine how far the average user is progressing in the game.
|
||||
<br>
|
||||
<strong>Tagging Best Practices</strong>
|
||||
<ul>
|
||||
<li>DO NOT use tags to record personally identifiable information.</li>
|
||||
<li>The best way to use tags is to create all the tag strings as predefined
|
||||
constants and only use those. This is more efficient and removes the risk of
|
||||
collecting personal information.</li>
|
||||
<li>Do not set tags inside loops or any other place which gets called
|
||||
frequently. This can cause a lot of data to be stored and uploaded.</li>
|
||||
</ul>
|
||||
<br>
|
||||
See the tagging guide at: http://wiki.localytics.com/
|
||||
@param event The name of the event which occurred.
|
||||
*/
|
||||
- (void)tagEvent:(NSString *)event;
|
||||
|
||||
- (void)tagEvent:(NSString *)event attributes:(NSDictionary *)attributes;
|
||||
|
||||
- (void)tagEvent:(NSString *)event attributes:(NSDictionary *)attributes reportAttributes:(NSDictionary *)reportAttributes;
|
||||
|
||||
/*!
|
||||
@method tagScreen
|
||||
@abstract Allows tagging the flow of screens encountered during the session.
|
||||
@param screen The name of the screen
|
||||
*/
|
||||
- (void)tagScreen:(NSString *)screen;
|
||||
|
||||
/*!
|
||||
@method upload
|
||||
@abstract Creates a low priority thread which uploads any Localytics data already stored
|
||||
on the device. This should be done early in the process life in order to
|
||||
guarantee as much time as possible for slow connections to complete. It is also reasonable
|
||||
to upload again when the application is exiting because if the upload is cancelled the data
|
||||
will just get uploaded the next time the app comes up.
|
||||
*/
|
||||
- (void)upload;
|
||||
|
||||
/*!
|
||||
@method setCustomDimension
|
||||
@abstract (ENTERPRISE ONLY) Sets the value of a custom dimension. Custom dimensions are dimensions
|
||||
which contain user defined data unlike the predefined dimensions such as carrier, model, and country.
|
||||
Once a value for a custom dimension is set, the device it was set on will continue to upload that value
|
||||
until the value is changed. To clear a value pass nil as the value.
|
||||
The proper use of custom dimensions involves defining a dimension with less than ten distinct possible
|
||||
values and assigning it to one of the four available custom dimensions. Once assigned this definition should
|
||||
never be changed without changing the App Key otherwise old installs of the application will pollute new data.
|
||||
*/
|
||||
- (void)setCustomDimension:(int)dimension value:(NSString *)value;
|
||||
|
||||
@end
|
||||
1148
Localytics/LocalyticsSession.m
Normal file
1148
Localytics/LocalyticsSession.m
Normal file
File diff suppressed because it is too large
Load Diff
42
Localytics/LocalyticsUploader.h
Normal file
42
Localytics/LocalyticsUploader.h
Normal file
@@ -0,0 +1,42 @@
|
||||
// LocalyticsUploader.h
|
||||
// Copyright (C) 2009 Char Software Inc., DBA Localytics
|
||||
//
|
||||
// This code is provided under the Localytics Modified BSD License.
|
||||
// A copy of this license has been distributed in a file called LICENSE
|
||||
// with this source code.
|
||||
//
|
||||
// Please visit www.localytics.com for more information.
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
/*!
|
||||
@class LocalyticsUploader
|
||||
@discussion Singleton class to handle data uploads
|
||||
*/
|
||||
|
||||
@interface LocalyticsUploader : NSObject {
|
||||
}
|
||||
|
||||
@property (readonly) BOOL isUploading;
|
||||
|
||||
/*!
|
||||
@method sharedLocalyticsUploader
|
||||
@abstract Establishes this as a Singleton Class allowing for data persistence.
|
||||
The class is accessed within the code using the following syntax:
|
||||
[[LocalyticsUploader sharedLocalyticsUploader] functionHere]
|
||||
*/
|
||||
+ (LocalyticsUploader *)sharedLocalyticsUploader;
|
||||
|
||||
/*!
|
||||
@method LocalyticsUploader
|
||||
@abstract Creates a thread which uploads all queued header and event data.
|
||||
All files starting with sessionFilePrefix are renamed,
|
||||
uploaded and deleted on upload. This way the sessions can continue
|
||||
writing data regardless of whether or not the upload succeeds. Files
|
||||
which have been renamed still count towards the total number of Localytics
|
||||
files which can be stored on the disk.
|
||||
@param localyticsApplicationKey the Localytics application ID
|
||||
*/
|
||||
- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey;
|
||||
|
||||
@end
|
||||
236
Localytics/LocalyticsUploader.m
Normal file
236
Localytics/LocalyticsUploader.m
Normal file
@@ -0,0 +1,236 @@
|
||||
// LocalyticsUploader.m
|
||||
// Copyright (C) 2009 Char Software Inc., DBA Localytics
|
||||
//
|
||||
// This code is provided under the Localytics Modified BSD License.
|
||||
// A copy of this license has been distributed in a file called LICENSE
|
||||
// with this source code.
|
||||
//
|
||||
// Please visit www.localytics.com for more information.
|
||||
|
||||
#import "LocalyticsUploader.h"
|
||||
#import "LocalyticsSession.h"
|
||||
#import "LocalyticsDatabase.h"
|
||||
#import <zlib.h>
|
||||
|
||||
#define LOCALYTICS_URL @"http://analytics.localytics.com/api/v2/applications/%@/uploads"
|
||||
|
||||
static LocalyticsUploader *_sharedUploader = nil;
|
||||
|
||||
@interface LocalyticsUploader ()
|
||||
- (void)finishUpload;
|
||||
- (NSData *)gzipDeflatedDataWithData:(NSData *)data;
|
||||
- (void)logMessage:(NSString *)message;
|
||||
|
||||
@property (readwrite) BOOL isUploading;
|
||||
|
||||
@end
|
||||
|
||||
@implementation LocalyticsUploader
|
||||
@synthesize isUploading = _isUploading;
|
||||
|
||||
#pragma mark - Singleton Class
|
||||
+ (LocalyticsUploader *)sharedLocalyticsUploader {
|
||||
@synchronized(self) {
|
||||
if (_sharedUploader == nil) {
|
||||
_sharedUploader = [[self alloc] init];
|
||||
}
|
||||
}
|
||||
return _sharedUploader;
|
||||
}
|
||||
|
||||
#pragma mark - Class Methods
|
||||
|
||||
- (void)uploaderWithApplicationKey:(NSString *)localyticsApplicationKey {
|
||||
|
||||
// Do nothing if already uploading.
|
||||
if (self.isUploading == true)
|
||||
{
|
||||
[self logMessage:@"Upload already in progress. Aborting."];
|
||||
return;
|
||||
}
|
||||
|
||||
[self logMessage:@"Beginning upload process"];
|
||||
self.isUploading = true;
|
||||
|
||||
// Prepare the data for upload. The upload could take a long time, so some effort has to be made to be sure that events
|
||||
// which get written while the upload is taking place don't get lost or duplicated. To achieve this, the logic is:
|
||||
// 1) Append every header row blob string and and those of its associated events to the upload string.
|
||||
// 2) Deflate and upload the data.
|
||||
// 3) On success, delete all blob headers and staged events. Events added while an upload is in process are not
|
||||
// deleted because they are not associated a header (and cannot be until the upload completes).
|
||||
|
||||
// Step 1
|
||||
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
|
||||
|
||||
LocalyticsDatabase *db = [LocalyticsDatabase sharedLocalyticsDatabase];
|
||||
NSString *blobString = [db uploadBlobString];
|
||||
|
||||
if ([blobString length] == 0) {
|
||||
// There is nothing outstanding to upload.
|
||||
[self logMessage:@"Abandoning upload. There are no new events."];
|
||||
[pool drain];
|
||||
[self finishUpload];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *requestData = [blobString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSString *myString = [[[NSString alloc] initWithData:requestData encoding:NSUTF8StringEncoding] autorelease];
|
||||
[self logMessage:[NSString stringWithFormat:@"Uploading data (length: %u)", [myString length]]];
|
||||
|
||||
// Step 2
|
||||
NSData *deflatedRequestData = [[self gzipDeflatedDataWithData:requestData] retain];
|
||||
|
||||
[pool drain];
|
||||
|
||||
NSString *apiUrlString = [NSString stringWithFormat:LOCALYTICS_URL, [localyticsApplicationKey stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
|
||||
NSMutableURLRequest *submitRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:apiUrlString]
|
||||
cachePolicy:NSURLRequestReloadIgnoringCacheData
|
||||
timeoutInterval:60.0];
|
||||
[submitRequest setHTTPMethod:@"POST"];
|
||||
[submitRequest setValue:@"application/x-gzip" forHTTPHeaderField:@"Content-Type"];
|
||||
[submitRequest setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"];
|
||||
[submitRequest setValue:[NSString stringWithFormat:@"%d", [deflatedRequestData length]] forHTTPHeaderField:@"Content-Length"];
|
||||
[submitRequest setHTTPBody:deflatedRequestData];
|
||||
[deflatedRequestData release];
|
||||
|
||||
// Perform synchronous upload in an async dispatch. This is necessary because the calling block will not persist to
|
||||
// receive the response data.
|
||||
dispatch_group_async([[LocalyticsSession sharedLocalyticsSession] criticalGroup], [[LocalyticsSession sharedLocalyticsSession] queue], ^{
|
||||
@try {
|
||||
NSURLResponse *response = nil;
|
||||
NSError *responseError = nil;
|
||||
[NSURLConnection sendSynchronousRequest:submitRequest returningResponse:&response error:&responseError];
|
||||
NSInteger responseStatusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
|
||||
if (responseError) {
|
||||
// On error, simply print the error and close the uploader. We have to assume the data was not transmited
|
||||
// so it is not deleted. In the event that we accidently store data which was succesfully uploaded, the
|
||||
// duplicate data will be ignored by the server when it is next uploaded.
|
||||
[self logMessage:[NSString stringWithFormat:
|
||||
@"Error Uploading. Code: %d, Description: %@",
|
||||
[responseError code],
|
||||
[responseError localizedDescription]]];
|
||||
} else {
|
||||
// Step 3
|
||||
// While response status codes in the 5xx range leave upload rows intact, the default case is to delete.
|
||||
if (responseStatusCode >= 500 && responseStatusCode < 600) {
|
||||
[self logMessage:[NSString stringWithFormat:@"Upload failed with response status code %d", responseStatusCode]];
|
||||
} else {
|
||||
// Because only one instance of the uploader can be running at a time it should not be possible for
|
||||
// new upload rows to appear so there is no fear of deleting data which has not yet been uploaded.
|
||||
[self logMessage:[NSString stringWithFormat:@"Upload completed successfully. Response code %d", responseStatusCode]];
|
||||
[[LocalyticsDatabase sharedLocalyticsDatabase] deleteUploadedData];
|
||||
}
|
||||
}
|
||||
}
|
||||
@catch (NSException * e) {}
|
||||
|
||||
[self finishUpload];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)finishUpload
|
||||
{
|
||||
self.isUploading = false;
|
||||
|
||||
// Upload data has been deleted. Recover the disk space if necessary.
|
||||
[[LocalyticsDatabase sharedLocalyticsDatabase] vacuumIfRequired];
|
||||
}
|
||||
|
||||
/*!
|
||||
@method gzipDeflatedDataWithData
|
||||
@abstract Deflates the provided data using gzip at the default compression level (6). Complete NSData gzip category available on CocoaDev. http://www.cocoadev.com/index.pl?NSDataCategory.
|
||||
@return the deflated data
|
||||
*/
|
||||
- (NSData *)gzipDeflatedDataWithData:(NSData *)data
|
||||
{
|
||||
if ([data length] == 0) return data;
|
||||
|
||||
z_stream strm;
|
||||
|
||||
strm.zalloc = Z_NULL;
|
||||
strm.zfree = Z_NULL;
|
||||
strm.opaque = Z_NULL;
|
||||
strm.total_out = 0;
|
||||
strm.next_in=(Bytef *)[data bytes];
|
||||
strm.avail_in = [data length];
|
||||
|
||||
// Compresssion Levels:
|
||||
// Z_NO_COMPRESSION
|
||||
// Z_BEST_SPEED
|
||||
// Z_BEST_COMPRESSION
|
||||
// Z_DEFAULT_COMPRESSION
|
||||
|
||||
if (deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (15+16), 8, Z_DEFAULT_STRATEGY) != Z_OK) return nil;
|
||||
|
||||
NSMutableData *compressed = [NSMutableData dataWithLength:16384]; // 16K chunks for expansion
|
||||
|
||||
do {
|
||||
|
||||
if (strm.total_out >= [compressed length])
|
||||
[compressed increaseLengthBy: 16384];
|
||||
|
||||
strm.next_out = [compressed mutableBytes] + strm.total_out;
|
||||
strm.avail_out = [compressed length] - strm.total_out;
|
||||
|
||||
deflate(&strm, Z_FINISH);
|
||||
|
||||
} while (strm.avail_out == 0);
|
||||
|
||||
deflateEnd(&strm);
|
||||
|
||||
[compressed setLength: strm.total_out];
|
||||
return [NSData dataWithData:compressed];
|
||||
}
|
||||
|
||||
/*!
|
||||
@method logMessage
|
||||
@abstract Logs a message with (localytics uploader) prepended to it
|
||||
@param message The message to log
|
||||
*/
|
||||
- (void) logMessage:(NSString *)message {
|
||||
if(DO_LOCALYTICS_LOGGING) {
|
||||
NSLog(@"(localytics uploader) %s\n", [message UTF8String]);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - System Functions
|
||||
+ (id)allocWithZone:(NSZone *)zone {
|
||||
@synchronized(self) {
|
||||
if (_sharedUploader == nil) {
|
||||
_sharedUploader = [super allocWithZone:zone];
|
||||
return _sharedUploader;
|
||||
}
|
||||
}
|
||||
// returns nil on subsequent allocations
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)retain {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (unsigned)retainCount {
|
||||
// maximum value of an unsigned int - prevents additional retains for the class
|
||||
return UINT_MAX;
|
||||
}
|
||||
|
||||
- (oneway void)release {
|
||||
// ignore release commands
|
||||
}
|
||||
|
||||
- (id)autorelease {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[_sharedUploader release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
48
Localytics/UploaderThread.h
Normal file
48
Localytics/UploaderThread.h
Normal file
@@ -0,0 +1,48 @@
|
||||
// UploaderThread.h
|
||||
// Copyright (C) 2009 Char Software Inc., DBA Localytics
|
||||
//
|
||||
// This code is provided under the Localytics Modified BSD License.
|
||||
// A copy of this license has been distributed in a file called LICENSE
|
||||
// with this source code.
|
||||
//
|
||||
// Please visit www.localytics.com for more information.
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
/*!
|
||||
@class UploaderThread
|
||||
@discussion Singleton class to handle data uploads
|
||||
*/
|
||||
|
||||
@interface UploaderThread : NSObject {
|
||||
NSURLConnection *_uploadConnection; // The connection which uploads the bits
|
||||
NSInteger _responseStatusCode; // The HTTP response status code for the current connection
|
||||
|
||||
BOOL _isUploading; // A flag to gaurantee only one uploader instance can happen at once
|
||||
}
|
||||
|
||||
@property (nonatomic, retain) NSURLConnection *uploadConnection;
|
||||
|
||||
@property BOOL isUploading;
|
||||
|
||||
/*!
|
||||
@method sharedUploaderThread
|
||||
@abstract Establishes this as a Singleton Class allowing for data persistence.
|
||||
The class is accessed within the code using the following syntax:
|
||||
[[UploaderThread sharedUploaderThread] functionHere]
|
||||
*/
|
||||
+ (UploaderThread *)sharedUploaderThread;
|
||||
|
||||
/*!
|
||||
@method UploaderThread
|
||||
@abstract Creates a thread which uploads all queued header and event data.
|
||||
All files starting with sessionFilePrefix are renamed,
|
||||
uploaded and deleted on upload. This way the sessions can continue
|
||||
writing data regardless of whether or not the upload succeeds. Files
|
||||
which have been renamed still count towards the total number of Localytics
|
||||
files which can be stored on the disk.
|
||||
@param localyticsApplicationKey the Localytics application ID
|
||||
*/
|
||||
- (void)uploaderThreadwithApplicationKey:(NSString *)localyticsApplicationKey;
|
||||
|
||||
@end
|
||||
260
Localytics/UploaderThread.m
Normal file
260
Localytics/UploaderThread.m
Normal file
@@ -0,0 +1,260 @@
|
||||
// UploaderThread.m
|
||||
// Copyright (C) 2009 Char Software Inc., DBA Localytics
|
||||
//
|
||||
// This code is provided under the Localytics Modified BSD License.
|
||||
// A copy of this license has been distributed in a file called LICENSE
|
||||
// with this source code.
|
||||
//
|
||||
// Please visit www.localytics.com for more information.
|
||||
|
||||
#import "UploaderThread.h"
|
||||
#import "LocalyticsSession.h"
|
||||
#import "LocalyticsDatabase.h"
|
||||
#import <zlib.h>
|
||||
|
||||
#define LOCALYTICS_URL @"http://analytics.localytics.com/api/v2/applications/%@/uploads" // url to send the
|
||||
|
||||
static UploaderThread *_sharedUploaderThread = nil;
|
||||
|
||||
@interface UploaderThread ()
|
||||
- (void)complete;
|
||||
- (NSData *)gzipDeflatedDataWithData:(NSData *)data;
|
||||
- (void)logMessage:(NSString *)message;
|
||||
@end
|
||||
|
||||
@implementation UploaderThread
|
||||
|
||||
@synthesize uploadConnection = _uploadConnection;
|
||||
@synthesize isUploading = _isUploading;
|
||||
|
||||
#pragma mark Singleton Class
|
||||
+ (UploaderThread *)sharedUploaderThread {
|
||||
@synchronized(self) {
|
||||
if (_sharedUploaderThread == nil)
|
||||
{
|
||||
_sharedUploaderThread = [[self alloc] init];
|
||||
}
|
||||
}
|
||||
return _sharedUploaderThread;
|
||||
}
|
||||
|
||||
#pragma mark Class Methods
|
||||
- (void)uploaderThreadwithApplicationKey:(NSString *)localyticsApplicationKey {
|
||||
|
||||
// Do nothing if already uploading.
|
||||
if (self.uploadConnection != nil || self.isUploading == true)
|
||||
{
|
||||
[self logMessage:@"Upload already in progress. Aborting."];
|
||||
return;
|
||||
}
|
||||
|
||||
[self logMessage:@"Beginning upload process"];
|
||||
self.isUploading = true;
|
||||
|
||||
// Prepare the data for upload. The upload could take a long time, so some effort has to be made to be sure that events
|
||||
// which get written while the upload is taking place don't get lost or duplicated. To achieve this, the logic is:
|
||||
// 1) Append every header row blob string and and those of its associated events to the upload string.
|
||||
// 2) Deflate and upload the data.
|
||||
// 3) On success, delete all blob headers and staged events. Events added while an upload is in process are not
|
||||
// deleted because they are not associated a header (and cannot be until the upload completes).
|
||||
|
||||
// Step 1
|
||||
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
|
||||
|
||||
LocalyticsDatabase *db = [LocalyticsDatabase sharedLocalyticsDatabase];
|
||||
NSString *blobString = [db uploadBlobString];
|
||||
|
||||
if ([blobString length] == 0) {
|
||||
// There is nothing outstanding to upload.
|
||||
[self logMessage:@"Abandoning upload. There are no new events."];
|
||||
|
||||
[pool drain];
|
||||
[self complete];
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *requestData = [blobString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSString *myString = [[[NSString alloc] initWithData:requestData encoding:NSUTF8StringEncoding] autorelease];
|
||||
[self logMessage:@"Upload data:"];
|
||||
[self logMessage:myString];
|
||||
|
||||
// Step 2
|
||||
NSData *deflatedRequestData = [[self gzipDeflatedDataWithData:requestData] retain];
|
||||
|
||||
[pool drain];
|
||||
|
||||
NSString *apiUrlString = [NSString stringWithFormat:LOCALYTICS_URL, [localyticsApplicationKey stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
|
||||
NSMutableURLRequest *submitRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:apiUrlString]
|
||||
cachePolicy:NSURLRequestReloadIgnoringCacheData
|
||||
timeoutInterval:60.0];
|
||||
[submitRequest setHTTPMethod:@"POST"];
|
||||
[submitRequest setValue:@"application/x-gzip" forHTTPHeaderField:@"Content-Type"];
|
||||
[submitRequest setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"];
|
||||
[submitRequest setValue:[NSString stringWithFormat:@"%d", [deflatedRequestData length]] forHTTPHeaderField:@"Content-Length"];
|
||||
[submitRequest setHTTPBody:deflatedRequestData];
|
||||
[deflatedRequestData release];
|
||||
|
||||
// The NSURLConnection Object automatically spawns its own thread as a default behavior.
|
||||
@try
|
||||
{
|
||||
[self logMessage:@"Spawning new thread for upload"];
|
||||
self.uploadConnection = [NSURLConnection connectionWithRequest:submitRequest delegate:self];
|
||||
|
||||
// Step 3 is handled by connectionDidFinishLoading.
|
||||
}
|
||||
@catch (NSException * e)
|
||||
{
|
||||
[self complete];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark **** NSURLConnection FUNCTIONS ****
|
||||
|
||||
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
|
||||
// Used to gather response data from server - Not utilized in this version
|
||||
}
|
||||
|
||||
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
|
||||
// Could receive multiple response callbacks, likely due to redirection.
|
||||
// Record status and act only when connection completes load.
|
||||
_responseStatusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
}
|
||||
|
||||
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
|
||||
// If the connection finished loading, the files should be deleted. While response status codes in the 5xx range
|
||||
// leave upload rows intact, the default case is to delete.
|
||||
if (_responseStatusCode >= 500 && _responseStatusCode < 600)
|
||||
{
|
||||
[self logMessage:[NSString stringWithFormat:@"Upload failed with response status code %d", _responseStatusCode]];
|
||||
} else
|
||||
{
|
||||
// The connection finished loading and uploaded data should be deleted. Because only one instance of the
|
||||
// uploader can be running at a time it should not be possible for new upload rows to appear so there is no
|
||||
// fear of deleting data which has not yet been uploaded.
|
||||
[self logMessage:[NSString stringWithFormat:@"Upload completed successfully. Response code %d", _responseStatusCode]];
|
||||
[[LocalyticsDatabase sharedLocalyticsDatabase] deleteUploadData];
|
||||
}
|
||||
|
||||
// Close upload session
|
||||
[self complete];
|
||||
}
|
||||
|
||||
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
|
||||
// On error, simply print the error and close the uploader. We have to assume the data was not transmited
|
||||
// so it is not deleted. In the event that we accidently store data which was succesfully uploaded, the
|
||||
// duplicate data will be ignored by the server when it is next uploaded.
|
||||
[self logMessage:[NSString stringWithFormat:
|
||||
@"Error Uploading. Code: %d, Description: %s",
|
||||
[error code],
|
||||
[error localizedDescription]]];
|
||||
|
||||
[self complete];
|
||||
}
|
||||
|
||||
/*!
|
||||
@method complete
|
||||
@abstract closes the upload connection and reports back to the session that the upload is complete
|
||||
*/
|
||||
- (void)complete {
|
||||
_responseStatusCode = 0;
|
||||
self.uploadConnection = nil;
|
||||
self.isUploading = false;
|
||||
}
|
||||
|
||||
/*!
|
||||
@method gzipDeflatedDataWithData
|
||||
@abstract Deflates the provided data using gzip at the default compression level (6). Complete NSData gzip category available on CocoaDev. http://www.cocoadev.com/index.pl?NSDataCategory.
|
||||
@return the deflated data
|
||||
*/
|
||||
- (NSData *)gzipDeflatedDataWithData:(NSData *)data
|
||||
{
|
||||
if ([data length] == 0) return data;
|
||||
|
||||
z_stream strm;
|
||||
|
||||
strm.zalloc = Z_NULL;
|
||||
strm.zfree = Z_NULL;
|
||||
strm.opaque = Z_NULL;
|
||||
strm.total_out = 0;
|
||||
strm.next_in=(Bytef *)[data bytes];
|
||||
strm.avail_in = [data length];
|
||||
|
||||
// Compresssion Levels:
|
||||
// Z_NO_COMPRESSION
|
||||
// Z_BEST_SPEED
|
||||
// Z_BEST_COMPRESSION
|
||||
// Z_DEFAULT_COMPRESSION
|
||||
|
||||
if (deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (15+16), 8, Z_DEFAULT_STRATEGY) != Z_OK) return nil;
|
||||
|
||||
NSMutableData *compressed = [NSMutableData dataWithLength:16384]; // 16K chunks for expansion
|
||||
|
||||
do {
|
||||
|
||||
if (strm.total_out >= [compressed length])
|
||||
[compressed increaseLengthBy: 16384];
|
||||
|
||||
strm.next_out = [compressed mutableBytes] + strm.total_out;
|
||||
strm.avail_out = [compressed length] - strm.total_out;
|
||||
|
||||
deflate(&strm, Z_FINISH);
|
||||
|
||||
} while (strm.avail_out == 0);
|
||||
|
||||
deflateEnd(&strm);
|
||||
|
||||
[compressed setLength: strm.total_out];
|
||||
return [NSData dataWithData:compressed];
|
||||
}
|
||||
|
||||
/*!
|
||||
@method logMessage
|
||||
@abstract Logs a message with (localytics uploader) prepended to it
|
||||
@param message The message to log
|
||||
*/
|
||||
- (void) logMessage:(NSString *)message {
|
||||
if(DO_LOCALYTICS_LOGGING) {
|
||||
NSLog(@"(localytics uploader) %s\n", [message UTF8String]);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark System Functions
|
||||
+ (id)allocWithZone:(NSZone *)zone {
|
||||
@synchronized(self) {
|
||||
if (_sharedUploaderThread == nil) {
|
||||
_sharedUploaderThread = [super allocWithZone:zone];
|
||||
return _sharedUploaderThread;
|
||||
}
|
||||
}
|
||||
// returns nil on subsequent allocations
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)retain {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (unsigned)retainCount {
|
||||
// maximum value of an unsigned int - prevents additional retains for the class
|
||||
return UINT_MAX;
|
||||
}
|
||||
|
||||
- (oneway void)release {
|
||||
// ignore release commands
|
||||
}
|
||||
|
||||
- (id)autorelease {
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[_uploadConnection release];
|
||||
[_sharedUploaderThread release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
111
Localytics/WebserviceConstants.h
Normal file
111
Localytics/WebserviceConstants.h
Normal file
@@ -0,0 +1,111 @@
|
||||
// WebserviceConstants.h
|
||||
// Copyright (C) 2009 Char Software Inc., DBA Localytics
|
||||
//
|
||||
// This code is provided under the Localytics Modified BSD License.
|
||||
// A copy of this license has been distributed in a file called LICENSE
|
||||
// with this source code.
|
||||
//
|
||||
// Please visit www.localytics.com for more information.
|
||||
|
||||
// The constants which are used to make up the JSON blob
|
||||
// To save disk space and network bandwidth all the keywords have been
|
||||
// abbreviated and are exploded by the server.
|
||||
|
||||
/*********************
|
||||
* Shared Attributes *
|
||||
*********************/
|
||||
#define PARAM_UUID @"u" // UUID for JSON document
|
||||
#define PARAM_DATA_TYPE @"dt" // Data Type
|
||||
#define PARAM_CLIENT_TIME @"ct" // Client Time, seconds from Unix epoch (int)
|
||||
#define PARAM_LATITUDE @"lat" // Latitude - if available
|
||||
#define PARAM_LONGITUDE @"lon" // Longitude - if available
|
||||
#define PARAM_SESSION_UUID @"su" // UUID for an existing session
|
||||
#define PARAM_NEW_SESSION_UUID @"u" // UUID for a new session
|
||||
#define PARAM_ATTRIBUTES @"attrs" // Attributes (dictionary)
|
||||
|
||||
/***************
|
||||
* Blob Header *
|
||||
***************/
|
||||
|
||||
// PARAM_UUID
|
||||
// PARAM_DATA_TYPE => "h" for Header
|
||||
// PARAM_ATTRIBUTES => dictionary containing Header Common Attributes
|
||||
#define PARAM_PERSISTED_AT @"pa" // Persistent Storage Created At. A timestamp created when the app was
|
||||
// first launched and the persistent storage was created. Stores as
|
||||
// seconds from Unix epoch. (int)
|
||||
#define PARAM_SEQUENCE_NUMBER @"seq" // Sequence number - an increasing count for each blob, stored in the
|
||||
// persistent store Consistent across app starts. (int)
|
||||
|
||||
/****************************
|
||||
* Header Common Attributes *
|
||||
****************************/
|
||||
|
||||
// PARAM_DATA_TYPE
|
||||
#define PARAM_APP_KEY @"au" // Localytics Application ID
|
||||
#define PARAM_DEVICE_UUID @"du" // Device UUID
|
||||
#define PARAM_DEVICE_UUID_HASHED @"udid" // Hashed version of the UUID
|
||||
#define PARAM_DEVICE_MAC @"wmac" // Hashed version of the device Mac
|
||||
#define PARAM_INSTALL_ID @"iu" // Install ID
|
||||
#define PARAM_JAILBROKEN @"j" // Jailbroken (boolean)
|
||||
#define PARAM_LIBRARY_VERSION @"lv" // Client Version
|
||||
#define PARAM_APP_VERSION @"av" // Application Version
|
||||
#define PARAM_DEVICE_PLATFORM @"dp" // Device Platform
|
||||
#define PARAM_LOCALE_LANGUAGE @"dll" // Locale Language
|
||||
#define PARAM_LOCALE_COUNTRY @"dlc" // Locale Country
|
||||
#define PARAM_NETWORK_COUNTRY @"nc" // Network Country (iso code) // ???: Never used on iPhone.
|
||||
#define PARAM_DEVICE_COUNTRY @"dc" // Device Country (iso code)
|
||||
#define PARAM_DEVICE_MANUFACTURER @"dma" // Device Manufacturer // ???: Never used on iPhone. Used to be "Device Make".
|
||||
#define PARAM_DEVICE_MODEL @"dmo" // Device Model
|
||||
#define PARAM_DEVICE_OS_VERSION @"dov" // Device OS Version
|
||||
#define PARAM_NETWORK_CARRIER @"nca" // Network Carrier
|
||||
#define PARAM_DATA_CONNECTION @"dac" // Data Connection Type // ???: Never used on iPhone.
|
||||
#define PARAM_OPT_VALUE @"optin" // Opt In (boolean)
|
||||
#define PARAM_DEVICE_MEMORY @"dmem" // Device Memory
|
||||
|
||||
/*****************
|
||||
* Session Start *
|
||||
*****************/
|
||||
|
||||
// PARAM_UUID
|
||||
// PARAM_DATA_TYPE => "s" for Start
|
||||
// PARAM_CLIENT_TIME
|
||||
#define PARAM_SESSION_NUMBER @"nth" // This is the nth session on the device, 1-indexed (int)
|
||||
|
||||
/****************
|
||||
* Session Stop *
|
||||
****************/
|
||||
|
||||
// PARAM_UUID
|
||||
// PARAM_DATA_TYPE => "c" for Close
|
||||
// PARAM_CLIENT_TIME
|
||||
// PARAM_LATITUDE
|
||||
// PARAM_LONGITUDE
|
||||
// PARAM_SESSION_UUID => UUID of session being closed
|
||||
#define PARAM_SESSION_ACTIVE @"cta" // Active time in seconds (time app was active)
|
||||
#define PARAM_SESSION_TOTAL @"ctl" // Total session length
|
||||
#define PARAM_SESSION_SCREENFLOW @"fl" // Screens encountered during this session, in order
|
||||
|
||||
/*********************
|
||||
* Application Event *
|
||||
*********************/
|
||||
|
||||
// PARAM_UUID
|
||||
// PARAM_DATA_TYPE => "e" for Event
|
||||
// PARAM_CLIENT_TIME
|
||||
// PARAM_LATITUDE
|
||||
// PARAM_LONGITUDE
|
||||
// PARAM_SESSION_UUID => UUID of session event occured in
|
||||
// PARAM_ATTRIBUTES => dictionary containing attributes for this event as key-value string pairs
|
||||
#define PARAM_EVENT_NAME @"n" // Event Name, (eg. 'Button Click')
|
||||
#define PARAM_REPORT_ATTRIBUTES @"rattrs" // Attributes used in custom reports
|
||||
|
||||
/********************
|
||||
* Application flow *
|
||||
********************/
|
||||
|
||||
// PARAM_UUID
|
||||
// PARAM_DATA_TYPE => "f" for Flow
|
||||
// PARAM_CLIENT_TIME
|
||||
#define PARAM_SESSION_START @"ss" // Start time for the current session.
|
||||
#define PARAM_NEW_FLOW_EVENTS @"nw" // Events and screens encountered during this session that have NOT been staged for upload.
|
||||
#define PARAM_OLD_FLOW_EVENTS @"od" // Events and screens encountered during this session that HAVE been staged for upload.
|
||||
6312
MasterPassword-Mac.xcodeproj/project.pbxproj
Normal file
6312
MasterPassword-Mac.xcodeproj/project.pbxproj
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,6 @@
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:MasterPassword.xcodeproj">
|
||||
location = "self:MasterPassword-Mac.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DAB8D987150374AD00CED3BC"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-Mac.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Debug">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DAB8D987150374AD00CED3BC"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-Mac.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Debug"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DAB8D987150374AD00CED3BC"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-Mac.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.coredata.ubiquity.logLevel 3"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Release"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DAB8D987150374AD00CED3BC"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-Mac.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
5106
MasterPassword-iOS.xcodeproj/project.pbxproj
Normal file
5106
MasterPassword-iOS.xcodeproj/project.pbxproj
Normal file
File diff suppressed because it is too large
Load Diff
7
MasterPassword-iOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
MasterPassword-iOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:MasterPassword-iOS.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "NO"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Debug">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "AppStore"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "AppStore"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "AppStore"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0430"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Debug">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DA3EF17815A47744003ABF4E"
|
||||
BuildableName = "Tests.octest"
|
||||
BlueprintName = "Tests"
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Debug"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
<AdditionalOption
|
||||
key = "NSZombieEnabled"
|
||||
value = "YES"
|
||||
isEnabled = "YES">
|
||||
</AdditionalOption>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Debug"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword-iOS.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "AdHoc"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
Binary file not shown.
Binary file not shown.
BIN
MasterPassword.sketch/QuickLook/Preview.png
Normal file
BIN
MasterPassword.sketch/QuickLook/Preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB |
BIN
MasterPassword.sketch/QuickLook/Thumbnail.png
Normal file
BIN
MasterPassword.sketch/QuickLook/Thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
File diff suppressed because it is too large
Load Diff
@@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
version = "1.3">
|
||||
<BuildAction>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForRunning = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<LaunchAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Production">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
</Scheme>
|
||||
@@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
version = "1.3">
|
||||
<BuildAction>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForRunning = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<LaunchAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Debug">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DA5BFA43147E415C00F98B1E"
|
||||
BuildableName = "MasterPassword.app"
|
||||
BlueprintName = "MasterPassword"
|
||||
ReferencedContainer = "container:MasterPassword.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
</Scheme>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
49
MasterPassword/Java/masterpassword-algorithm/pom.xml
Normal file
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
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
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
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
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
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
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
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
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
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
|
||||
@@ -1,34 +0,0 @@
|
||||
//
|
||||
// MPAppDelegate.h
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface MPAppDelegate : PearlAppDelegate
|
||||
|
||||
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
|
||||
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
|
||||
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
|
||||
@property (readonly, strong, nonatomic) NSData *keyPhrase;
|
||||
@property (readonly, strong, nonatomic) NSData *keyPhraseHash;
|
||||
@property (readonly, strong, nonatomic) NSString *keyPhraseHashHex;
|
||||
|
||||
+ (MPAppDelegate *)get;
|
||||
+ (NSManagedObjectModel *)managedObjectModel;
|
||||
+ (NSManagedObjectContext *)managedObjectContext;
|
||||
|
||||
- (void)saveContext;
|
||||
- (NSURL *)applicationDocumentsDirectory;
|
||||
|
||||
- (void)showGuide;
|
||||
- (void)loadKeyPhrase:(BOOL)animated;
|
||||
- (void)signOut;
|
||||
- (void)forgetKeyPhrase;
|
||||
- (NSData *)keyPhraseWithLength:(NSUInteger)keyLength;
|
||||
- (BOOL)tryMasterPassword:(NSString *)tryPassword;
|
||||
|
||||
@end
|
||||
@@ -1,521 +0,0 @@
|
||||
//
|
||||
// MPAppDelegate.m
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPAppDelegate.h"
|
||||
|
||||
#import "MPMainViewController.h"
|
||||
#import "IASKSettingsReader.h"
|
||||
|
||||
@interface MPAppDelegate ()
|
||||
|
||||
@property (strong, nonatomic) NSData *keyPhrase;
|
||||
@property (strong, nonatomic) NSData *keyPhraseHash;
|
||||
@property (strong, nonatomic) NSString *keyPhraseHashHex;
|
||||
|
||||
+ (NSDictionary *)keyPhraseQuery;
|
||||
+ (NSDictionary *)keyPhraseHashQuery;
|
||||
|
||||
- (void)loadStoredKeyPhrase;
|
||||
- (void)askKeyPhrase:(BOOL)animated;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MPAppDelegate
|
||||
|
||||
@synthesize managedObjectModel = __managedObjectModel;
|
||||
@synthesize managedObjectContext = __managedObjectContext;
|
||||
@synthesize persistentStoreCoordinator = __persistentStoreCoordinator;
|
||||
|
||||
@synthesize keyPhrase = _keyPhrase;
|
||||
@synthesize keyPhraseHash = _keyPhraseHash;
|
||||
@synthesize keyPhraseHashHex = _keyPhraseHashHex;
|
||||
|
||||
+ (void)initialize {
|
||||
|
||||
[MPConfig get];
|
||||
|
||||
#ifdef DEBUG
|
||||
[PearlLogger get].autoprintLevel = PearlLogLevelTrace;
|
||||
[NSClassFromString(@"WebView") performSelector:@selector(_enableRemoteInspector)];
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (NSDictionary *)keyPhraseQuery {
|
||||
|
||||
static NSDictionary *MPKeyPhraseQuery = nil;
|
||||
if (!MPKeyPhraseQuery)
|
||||
MPKeyPhraseQuery = [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
||||
attributes:[NSDictionary dictionaryWithObject:@"MasterPassword"
|
||||
forKey:(__bridge id)kSecAttrService]
|
||||
matches:nil];
|
||||
|
||||
return MPKeyPhraseQuery;
|
||||
}
|
||||
|
||||
+ (NSDictionary *)keyPhraseHashQuery {
|
||||
|
||||
static NSDictionary *MPKeyPhraseHashQuery = nil;
|
||||
if (!MPKeyPhraseHashQuery)
|
||||
MPKeyPhraseHashQuery = [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
||||
attributes:[NSDictionary dictionaryWithObject:@"MasterPasswordHash"
|
||||
forKey:(__bridge id)kSecAttrService]
|
||||
matches:nil];
|
||||
|
||||
return MPKeyPhraseHashQuery;
|
||||
}
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
|
||||
#ifndef PRODUCTION
|
||||
@try {
|
||||
[TestFlight takeOff:@"bd44885deee7adce0645ce8e5498d80a_NDQ5NDQyMDExLTEyLTAyIDExOjM1OjQ4LjQ2NjM4NA"];
|
||||
[TestFlight setOptions:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[NSNumber numberWithBool:NO], @"logToConsole",
|
||||
[NSNumber numberWithBool:NO], @"logToSTDERR",
|
||||
nil]];
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointLaunched];
|
||||
[[PearlLogger get] registerListener:^BOOL(PearlLogMessage *message) {
|
||||
if (message.level >= PearlLogLevelInfo)
|
||||
TFLog(@"%@", message);
|
||||
|
||||
return YES;
|
||||
}];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
err(@"TestFlight: %@", exception);
|
||||
}
|
||||
#endif
|
||||
|
||||
UIImage *navBarImage = [[UIImage imageNamed:@"ui_navbar_container"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 5)];
|
||||
[[UINavigationBar appearance] setBackgroundImage:navBarImage forBarMetrics:UIBarMetricsDefault];
|
||||
[[UINavigationBar appearance] setBackgroundImage:navBarImage forBarMetrics:UIBarMetricsLandscapePhone];
|
||||
[[UINavigationBar appearance] setTitleTextAttributes:
|
||||
[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[UIColor colorWithRed:255.0/255.0 green:255.0/255.0 blue:255.0/255.0 alpha:1.0], UITextAttributeTextColor,
|
||||
[UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.8], UITextAttributeTextShadowColor,
|
||||
[NSValue valueWithUIOffset:UIOffsetMake(0, -1)], UITextAttributeTextShadowOffset,
|
||||
[UIFont fontWithName:@"Helvetica-Neue" size:0.0], UITextAttributeFont,
|
||||
nil]];
|
||||
|
||||
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)];
|
||||
[[UIBarButtonItem appearance] setBackgroundImage:navBarButton forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
||||
[[UIBarButtonItem appearance] setBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];
|
||||
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:navBarBack forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
||||
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:nil forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];
|
||||
[[UIBarButtonItem appearance] setTitleTextAttributes:
|
||||
[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0], UITextAttributeTextColor,
|
||||
[UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5], UITextAttributeTextShadowColor,
|
||||
[NSValue valueWithUIOffset:UIOffsetMake(0, 1)], UITextAttributeTextShadowOffset,
|
||||
[UIFont fontWithName:@"Helvetica-Neue" size:0.0], UITextAttributeFont,
|
||||
nil]
|
||||
forState:UIControlStateNormal];
|
||||
|
||||
UIImage *toolBarImage = [[UIImage imageNamed:@"ui_toolbar_container"] resizableImageWithCapInsets:UIEdgeInsetsMake(25, 5, 5, 5)];
|
||||
[[UISearchBar appearance] setBackgroundImage:toolBarImage];
|
||||
[[UIToolbar appearance] setBackgroundImage:toolBarImage forToolbarPosition:UIToolbarPositionAny barMetrics:UIBarMetricsDefault];
|
||||
|
||||
/*
|
||||
UIImage *minImage = [[UIImage imageNamed:@"slider-minimum.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 0)];
|
||||
UIImage *maxImage = [[UIImage imageNamed:@"slider-maximum.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 0)];
|
||||
UIImage *thumbImage = [UIImage imageNamed:@"slider-handle.png"];
|
||||
|
||||
[[UISlider appearance] setMaximumTrackImage:maxImage forState:UIControlStateNormal];
|
||||
[[UISlider appearance] setMinimumTrackImage:minImage forState:UIControlStateNormal];
|
||||
[[UISlider appearance] setThumbImage:thumbImage forState:UIControlStateNormal];
|
||||
|
||||
UIImage *segmentSelected = [[UIImage imageNamed:@"segcontrol_sel.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
|
||||
UIImage *segmentUnselected = [[UIImage imageNamed:@"segcontrol_uns.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 15, 0, 15)];
|
||||
UIImage *segmentSelectedUnselected = [UIImage imageNamed:@"segcontrol_sel-uns.png"];
|
||||
UIImage *segUnselectedSelected = [UIImage imageNamed:@"segcontrol_uns-sel.png"];
|
||||
UIImage *segmentUnselectedUnselected = [UIImage imageNamed:@"segcontrol_uns-uns.png"];
|
||||
|
||||
[[UISegmentedControl appearance] setBackgroundImage:segmentUnselected forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
||||
[[UISegmentedControl appearance] setBackgroundImage:segmentSelected forState:UIControlStateSelected 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:segUnselectedSelected forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateSelected barMetrics:UIBarMetricsDefault];*/
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:kIASKAppSettingChanged object:nil queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
if ([NSStringFromSelector(@selector(storeKeyPhrase))
|
||||
isEqualToString:[note.object description]]) {
|
||||
self.keyPhrase = self.keyPhrase;
|
||||
[self loadKeyPhrase:YES];
|
||||
}
|
||||
if ([NSStringFromSelector(@selector(forgetKeyPhrase))
|
||||
isEqualToString:[note.object description]])
|
||||
[self loadKeyPhrase:YES];
|
||||
}];
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[PearlAlert showAlertWithTitle:@"Welcome, tester!" message:
|
||||
@"Thank you for taking the time to test Master Password.\n\n"
|
||||
@"Please provide any feedback, however minor it may seem, via the Feedback action item accessible from the top right.\n\n"
|
||||
@"Contact me directly at:\n"
|
||||
@"lhunath@lyndir.com\n"
|
||||
@"Or report detailed issues at:\n"
|
||||
@"https://youtrack.lyndir.com\n"
|
||||
viewStyle:UIAlertViewStyleDefault tappedButtonBlock:nil
|
||||
cancelTitle:nil otherTitles:[PearlStrings get].commonButtonOkay, nil];
|
||||
#endif
|
||||
|
||||
[[UIApplication sharedApplication] setStatusBarHidden:NO
|
||||
withAnimation:UIStatusBarAnimationSlide];
|
||||
|
||||
return [super application:application didFinishLaunchingWithOptions:launchOptions];
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(UIApplication *)application {
|
||||
|
||||
if ([[MPConfig get].showQuickStart boolValue])
|
||||
[self showGuide];
|
||||
else
|
||||
[self loadKeyPhrase:NO];
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointActivated];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)showGuide {
|
||||
|
||||
[self.navigationController performSegueWithIdentifier:@"MP_Guide" sender:self];
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointShowGuide];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)loadKeyPhrase:(BOOL)animated {
|
||||
|
||||
if (self.keyPhrase)
|
||||
return;
|
||||
|
||||
[self loadStoredKeyPhrase];
|
||||
if (!self.keyPhrase) {
|
||||
// Key phrase is not known. Ask user to set/specify it.
|
||||
dbg(@"Key phrase not known. Will ask user.");
|
||||
[self askKeyPhrase:animated];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)forgetKeyPhrase {
|
||||
|
||||
dbg(@"Forgetting key phrase.");
|
||||
[PearlAlert showAlertWithTitle:@"Changing Master Password"
|
||||
message:
|
||||
@"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"
|
||||
@"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]) {
|
||||
// Key phrase reset. Delete it.
|
||||
dbg(@"Deleting master key phrase and hash from key chain.");
|
||||
[PearlKeyChain deleteItemForQuery:[MPAppDelegate keyPhraseQuery]];
|
||||
[PearlKeyChain deleteItemForQuery:[MPAppDelegate keyPhraseHashQuery]];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyForgotten object:self];
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPForgotten];
|
||||
#endif
|
||||
}
|
||||
|
||||
[self loadKeyPhrase:YES];
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPChanged];
|
||||
#endif
|
||||
}
|
||||
cancelTitle:[PearlStrings get].commonButtonAbort
|
||||
otherTitles:[PearlStrings get].commonButtonContinue, nil];
|
||||
}
|
||||
|
||||
- (void)signOut {
|
||||
|
||||
self.keyPhrase = nil;
|
||||
[self loadKeyPhrase:YES];
|
||||
}
|
||||
|
||||
- (void)loadStoredKeyPhrase {
|
||||
|
||||
if ([[MPConfig get].storeKeyPhrase boolValue]) {
|
||||
// Key phrase is stored in keychain. Load it.
|
||||
dbg(@"Loading master key phrase from key chain.");
|
||||
self.keyPhrase = [PearlKeyChain dataOfItemForQuery:[MPAppDelegate keyPhraseQuery]];
|
||||
dbg(@" -> Master key phrase %@.", self.keyPhrase? @"found": @"NOT found");
|
||||
} else {
|
||||
// Key phrase should not be stored in keychain. Delete it.
|
||||
dbg(@"Deleting master key phrase from key chain.");
|
||||
[PearlKeyChain deleteItemForQuery:[MPAppDelegate keyPhraseQuery]];
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPUnstored];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (void)askKeyPhrase:(BOOL)animated {
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.navigationController presentViewController:
|
||||
[self.navigationController.storyboard instantiateViewControllerWithIdentifier:@"MPUnlockViewController"]
|
||||
animated:animated completion:nil];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(UIApplication *)application {
|
||||
|
||||
[self saveContext];
|
||||
|
||||
if (![[MPConfig get].rememberKeyPhrase boolValue])
|
||||
self.keyPhrase = nil;
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointDeactivated];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(UIApplication *)application {
|
||||
|
||||
[self saveContext];
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointTerminated];
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (MPAppDelegate *)get {
|
||||
|
||||
return (MPAppDelegate *)[super get];
|
||||
}
|
||||
|
||||
+ (NSManagedObjectContext *)managedObjectContext {
|
||||
|
||||
return [(MPAppDelegate *)[UIApplication sharedApplication].delegate managedObjectContext];
|
||||
}
|
||||
|
||||
+ (NSManagedObjectModel *)managedObjectModel {
|
||||
|
||||
return [(MPAppDelegate *)[UIApplication sharedApplication].delegate managedObjectModel];
|
||||
}
|
||||
|
||||
- (void)saveContext {
|
||||
|
||||
[self.managedObjectContext performBlock:^{
|
||||
NSError *error = nil;
|
||||
if ([self.managedObjectContext hasChanges] && ![self.managedObjectContext save:&error])
|
||||
err(@"Unresolved error %@", error);
|
||||
}];
|
||||
}
|
||||
|
||||
- (BOOL)tryMasterPassword:(NSString *)tryPassword {
|
||||
|
||||
NSData *keyPhraseHash = [PearlKeyChain dataOfItemForQuery:[MPAppDelegate keyPhraseHashQuery]];
|
||||
dbg(@"Key phrase hash %@.", keyPhraseHash? @"known": @"NOT known");
|
||||
|
||||
if (![tryPassword length])
|
||||
return NO;
|
||||
|
||||
NSData *tryKeyPhrase = keyPhraseForPassword(tryPassword);
|
||||
NSData *tryKeyPhraseHash = keyPhraseHashForKeyPhrase(tryKeyPhrase);
|
||||
if (keyPhraseHash)
|
||||
// A key phrase hash is known -> a key phrase is set.
|
||||
// Make sure the user's entered key phrase matches it.
|
||||
if (![keyPhraseHash isEqual:tryKeyPhraseHash]) {
|
||||
dbg(@"Key phrase hash mismatch. Expected: %@, answer: %@.", keyPhraseHash, tryKeyPhraseHash);
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPMismatch];
|
||||
#endif
|
||||
return NO;
|
||||
}
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPAsked];
|
||||
#endif
|
||||
|
||||
self.keyPhrase = tryKeyPhrase;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)setKeyPhrase:(NSData *)keyPhrase {
|
||||
|
||||
_keyPhrase = keyPhrase;
|
||||
|
||||
if (keyPhrase)
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeySet object:self];
|
||||
else
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyUnset object:self];
|
||||
|
||||
if (keyPhrase) {
|
||||
self.keyPhraseHash = keyPhraseHashForKeyPhrase(keyPhrase);
|
||||
self.keyPhraseHashHex = [self.keyPhraseHash encodeHex];
|
||||
|
||||
dbg(@"Updating master key phrase hash to: %@.", self.keyPhraseHashHex);
|
||||
[PearlKeyChain addOrUpdateItemForQuery:[MPAppDelegate keyPhraseHashQuery]
|
||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
self.keyPhraseHash, (__bridge id)kSecValueData,
|
||||
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
|
||||
nil]];
|
||||
if ([[MPConfig get].storeKeyPhrase boolValue]) {
|
||||
dbg(@"Storing master key phrase in key chain.");
|
||||
[PearlKeyChain addOrUpdateItemForQuery:[MPAppDelegate keyPhraseQuery]
|
||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
keyPhrase, (__bridge id)kSecValueData,
|
||||
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
|
||||
nil]];
|
||||
}
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:[NSString stringWithFormat:MPTestFlightCheckpointSetKeyphraseLength, _keyPhrase.length]];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (NSData *)keyPhraseWithLength:(NSUInteger)keyLength {
|
||||
|
||||
return [self.keyPhrase subdataWithRange:NSMakeRange(0, MIN(keyLength, self.keyPhrase.length))];
|
||||
}
|
||||
|
||||
#pragma mark - Core Data stack
|
||||
|
||||
/**
|
||||
Returns the managed object context for the application.
|
||||
If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application.
|
||||
*/
|
||||
- (NSManagedObjectContext *)managedObjectContext
|
||||
{
|
||||
if (__managedObjectContext)
|
||||
return __managedObjectContext;
|
||||
|
||||
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
|
||||
if (coordinator) {
|
||||
__managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
|
||||
__managedObjectContext.persistentStoreCoordinator = coordinator;
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:NSPersistentStoreDidImportUbiquitousContentChangesNotification
|
||||
object:coordinator
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
dbg(@"Ubiquitous content change: %@", note);
|
||||
|
||||
[__managedObjectContext performBlock:^{
|
||||
[__managedObjectContext mergeChangesFromContextDidSaveNotification:note];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotification:
|
||||
[NSNotification notificationWithName:UIScreenModeDidChangeNotification
|
||||
object:self userInfo:[note userInfo]]];
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
return __managedObjectContext;
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the managed object model for the application.
|
||||
If the model doesn't already exist, it is created from the application's model.
|
||||
*/
|
||||
- (NSManagedObjectModel *)managedObjectModel
|
||||
{
|
||||
if (__managedObjectModel)
|
||||
return __managedObjectModel;
|
||||
|
||||
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"MasterPassword" withExtension:@"momd"];
|
||||
return __managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the persistent store coordinator for the application.
|
||||
If the coordinator doesn't already exist, it is created and the application's store added to it.
|
||||
*/
|
||||
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
|
||||
{
|
||||
if (__persistentStoreCoordinator)
|
||||
return __persistentStoreCoordinator;
|
||||
|
||||
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"MasterPassword.sqlite"];
|
||||
|
||||
__persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
|
||||
[__persistentStoreCoordinator lock];
|
||||
NSError *error = nil;
|
||||
if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL
|
||||
options:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
|
||||
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
|
||||
@"MasterPassword.store", NSPersistentStoreUbiquitousContentNameKey,
|
||||
[[[NSFileManager defaultManager]
|
||||
URLForUbiquityContainerIdentifier:nil]
|
||||
URLByAppendingPathComponent:@"store"
|
||||
isDirectory:YES], NSPersistentStoreUbiquitousContentURLKey,
|
||||
nil]
|
||||
error:&error])
|
||||
{
|
||||
/*
|
||||
Replace this implementation with code to handle the error appropriately.
|
||||
|
||||
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
|
||||
Typical reasons for an error here include:
|
||||
* The persistent store is not accessible;
|
||||
* The schema for the persistent store is incompatible with current managed object model.
|
||||
Check the error message to determine what the actual problem was.
|
||||
|
||||
|
||||
If the persistent store is not accessible, there is typically something wrong with the file path. Often, a file URL is pointing into the application's resources directory instead of a writeable directory.
|
||||
|
||||
If you encounter schema incompatibility errors during development, you can reduce their frequency by:
|
||||
* Simply deleting the existing store:
|
||||
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil]
|
||||
|
||||
* Performing automatic lightweight migration by passing the following dictionary as the options parameter:
|
||||
[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
|
||||
|
||||
Lightweight migration will only work for a limited set of schema changes; consult "Core Data Model Versioning and Data Migration Programming Guide" for details.
|
||||
|
||||
*/
|
||||
err(@"Unresolved error %@, %@", error, [error userInfo]);
|
||||
#if DEBUG
|
||||
wrn(@"Deleted datastore: %@", storeURL);
|
||||
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil];
|
||||
#endif
|
||||
|
||||
#ifndef PRODUCTION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointStoreIncompatible];
|
||||
#endif
|
||||
|
||||
@throw [NSException exceptionWithName:error.domain reason:error.localizedDescription
|
||||
userInfo:[NSDictionary dictionaryWithObject:error forKey:@"cause"]];
|
||||
}
|
||||
|
||||
if (![[NSFileManager defaultManager] setAttributes:[NSDictionary dictionaryWithObject:NSFileProtectionComplete
|
||||
forKey:NSFileProtectionKey]
|
||||
ofItemAtPath:storeURL.path error:&error])
|
||||
err(@"Unresolved error %@, %@", error, [error userInfo]);
|
||||
[__persistentStoreCoordinator unlock];
|
||||
|
||||
return __persistentStoreCoordinator;
|
||||
}
|
||||
|
||||
#pragma mark - Application's Documents directory
|
||||
|
||||
/**
|
||||
Returns the URL to the application's Documents directory.
|
||||
*/
|
||||
- (NSURL *)applicationDocumentsDirectory
|
||||
{
|
||||
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
|
||||
}
|
||||
|
||||
@end
|
||||
19
MasterPassword/MPAppDelegate_Key.h
Normal file
19
MasterPassword/MPAppDelegate_Key.h
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// MPAppDelegate_Key.h
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPAppDelegate_Shared.h"
|
||||
|
||||
@interface MPAppDelegate_Shared (Key)
|
||||
|
||||
- (BOOL)signInAsUser:(MPUserEntity *)user usingMasterPassword:(NSString *)password;
|
||||
- (void)signOutAnimated:(BOOL)animated;
|
||||
|
||||
- (void)storeSavedKeyFor:(MPUserEntity *)user;
|
||||
- (void)forgetSavedKeyFor:(MPUserEntity *)user;
|
||||
|
||||
@end
|
||||
176
MasterPassword/MPAppDelegate_Key.m
Normal file
176
MasterPassword/MPAppDelegate_Key.m
Normal file
@@ -0,0 +1,176 @@
|
||||
//
|
||||
// MPAppDelegate.m
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Crashlytics/Crashlytics.h>
|
||||
#import "MPAppDelegate_Key.h"
|
||||
#import "MPAppDelegate_Store.h"
|
||||
#import "LocalyticsSession.h"
|
||||
|
||||
@implementation MPAppDelegate_Shared (Key)
|
||||
|
||||
static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
|
||||
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
||||
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
@"Saved Master Password", (__bridge id)kSecAttrService,
|
||||
user.name, (__bridge id)kSecAttrAccount,
|
||||
nil]
|
||||
matches:nil];
|
||||
}
|
||||
|
||||
- (MPKey *)loadSavedKeyFor:(MPUserEntity *)user {
|
||||
|
||||
NSData *keyData = [PearlKeyChain dataOfItemForQuery:keyQuery(user)];
|
||||
if (keyData)
|
||||
inf(@"Found key in keychain for: %@", user.userID);
|
||||
|
||||
else {
|
||||
user.saveKey = NO;
|
||||
inf(@"No key found in keychain for: %@", user.userID);
|
||||
}
|
||||
|
||||
return [MPAlgorithmDefault keyFromKeyData:keyData];
|
||||
}
|
||||
|
||||
- (void)storeSavedKeyFor:(MPUserEntity *)user {
|
||||
|
||||
if (user.saveKey) {
|
||||
NSData *existingKeyData = [PearlKeyChain dataOfItemForQuery:keyQuery(user)];
|
||||
|
||||
if (![existingKeyData isEqualToData:self.key.keyData]) {
|
||||
inf(@"Saving key in keychain for: %@", user.userID);
|
||||
|
||||
[PearlKeyChain addOrUpdateItemForQuery:keyQuery(user)
|
||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
self.key.keyData, (__bridge id)kSecValueData,
|
||||
#if TARGET_OS_IPHONE
|
||||
(__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly, (__bridge id)kSecAttrAccessible,
|
||||
#endif
|
||||
nil]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)forgetSavedKeyFor:(MPUserEntity *)user {
|
||||
|
||||
OSStatus result = [PearlKeyChain deleteItemForQuery:keyQuery(user)];
|
||||
if (result == noErr || result == errSecItemNotFound) {
|
||||
user.saveKey = NO;
|
||||
|
||||
if (result == noErr) {
|
||||
inf(@"Removed key from keychain for: %@", user.userID);
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyForgotten object:self];
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPCheckpointForgetSavedKey];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (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];
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Check the given master password string.
|
||||
if (!tryKey) {
|
||||
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];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
25
MasterPassword/MPAppDelegate_Shared.h
Normal file
25
MasterPassword/MPAppDelegate_Shared.h
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// MPAppDelegate_Shared.h
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPEntities.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
|
||||
@interface MPAppDelegate_Shared : PearlAppDelegate
|
||||
#else
|
||||
@interface MPAppDelegate_Shared : NSObject <PearlConfigDelegate>
|
||||
#endif
|
||||
|
||||
@property (strong, nonatomic) MPUserEntity *activeUser;
|
||||
@property (strong, nonatomic) MPKey *key;
|
||||
|
||||
+ (MPAppDelegate_Shared *)get;
|
||||
|
||||
- (NSURL *)applicationFilesDirectory;
|
||||
|
||||
@end
|
||||
44
MasterPassword/MPAppDelegate_Shared.m
Normal file
44
MasterPassword/MPAppDelegate_Shared.m
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// MPAppDelegate.m
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPAppDelegate_Shared.h"
|
||||
|
||||
@implementation MPAppDelegate_Shared
|
||||
|
||||
@synthesize key;
|
||||
@synthesize activeUser;
|
||||
|
||||
+ (MPAppDelegate_Shared *)get {
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
return (MPAppDelegate_Shared *)[UIApplication sharedApplication].delegate;
|
||||
#elif defined (__MAC_OS_X_VERSION_MIN_REQUIRED)
|
||||
return (MPAppDelegate_Shared *)[NSApplication sharedApplication].delegate;
|
||||
#else
|
||||
#error Unsupported OS.
|
||||
#endif
|
||||
}
|
||||
|
||||
- (NSURL *)applicationFilesDirectory {
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
|
||||
#else
|
||||
NSURL *appSupportURL = [[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject];
|
||||
NSURL *applicationFilesDirectory = [appSupportURL URLByAppendingPathComponent:@"com.lyndir.lhunath.MasterPassword"];
|
||||
|
||||
NSError *error = nil;
|
||||
[[NSFileManager defaultManager] createDirectoryAtURL:applicationFilesDirectory withIntermediateDirectories:YES attributes:nil error:&error];
|
||||
if (error)
|
||||
err(@"Couldn't create application directory: %@, error occurred: %@", applicationFilesDirectory, error);
|
||||
|
||||
return applicationFilesDirectory;
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
35
MasterPassword/MPAppDelegate_Store.h
Normal file
35
MasterPassword/MPAppDelegate_Store.h
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// MPAppDelegate_Key.h
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPAppDelegate_Shared.h"
|
||||
|
||||
#import "UbiquityStoreManager.h"
|
||||
|
||||
typedef enum {
|
||||
MPImportResultSuccess,
|
||||
MPImportResultCancelled,
|
||||
MPImportResultInvalidPassword,
|
||||
MPImportResultMalformedInput,
|
||||
MPImportResultInternalError,
|
||||
} MPImportResult;
|
||||
|
||||
@interface MPAppDelegate_Shared (Store)<UbiquityStoreManagerDelegate>
|
||||
|
||||
+ (NSManagedObjectContext *)managedObjectContextIfReady;
|
||||
+ (NSManagedObjectModel *)managedObjectModel;
|
||||
- (NSManagedObjectContext *)managedObjectContextIfReady;
|
||||
- (NSManagedObjectModel *)managedObjectModel;
|
||||
|
||||
- (UbiquityStoreManager *)storeManager;
|
||||
- (void)saveContext;
|
||||
|
||||
- (MPImportResult)importSites:(NSString *)importedSitesString withPassword:(NSString *)password
|
||||
askConfirmation:(BOOL(^)(NSUInteger importCount, NSUInteger deleteCount))confirmation;
|
||||
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords;
|
||||
|
||||
@end
|
||||
454
MasterPassword/MPAppDelegate_Store.m
Normal file
454
MasterPassword/MPAppDelegate_Store.m
Normal file
@@ -0,0 +1,454 @@
|
||||
//
|
||||
// MPAppDelegate.m
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPAppDelegate_Store.h"
|
||||
#import "LocalyticsSession.h"
|
||||
|
||||
@implementation MPAppDelegate_Shared (Store)
|
||||
|
||||
#pragma mark - Core Data setup
|
||||
|
||||
+ (NSManagedObjectContext *)managedObjectContextIfReady {
|
||||
|
||||
return [[self get] managedObjectContextIfReady];
|
||||
}
|
||||
|
||||
+ (NSManagedObjectModel *)managedObjectModel {
|
||||
|
||||
return [[self get] managedObjectModel];
|
||||
}
|
||||
|
||||
- (NSManagedObjectModel *)managedObjectModel {
|
||||
|
||||
static NSManagedObjectModel *managedObjectModel = nil;
|
||||
if (managedObjectModel)
|
||||
return managedObjectModel;
|
||||
|
||||
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"MasterPassword" withExtension:@"momd"];
|
||||
return managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
|
||||
}
|
||||
|
||||
- (NSManagedObjectContext *)managedObjectContextIfReady {
|
||||
|
||||
if (![self storeManager].isReady)
|
||||
return nil;
|
||||
|
||||
static NSManagedObjectContext *managedObjectContext = nil;
|
||||
if (managedObjectContext)
|
||||
return managedObjectContext;
|
||||
|
||||
managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
|
||||
[managedObjectContext performBlockAndWait:^{
|
||||
managedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
|
||||
managedObjectContext.persistentStoreCoordinator = [self storeManager].persistentStoreCoordinator;
|
||||
managedObjectContext.undoManager = [NSUndoManager new];
|
||||
}];
|
||||
|
||||
return managedObjectContext;
|
||||
}
|
||||
|
||||
- (UbiquityStoreManager *)storeManager {
|
||||
|
||||
static UbiquityStoreManager *storeManager = nil;
|
||||
if (storeManager)
|
||||
return storeManager;
|
||||
|
||||
storeManager = [[UbiquityStoreManager alloc] initWithManagedObjectModel:[self managedObjectModel]
|
||||
localStoreURL:[[self applicationFilesDirectory] URLByAppendingPathComponent:@"MasterPassword.sqlite"]
|
||||
containerIdentifier:@"HL3Q45LX9N.com.lyndir.lhunath.MasterPassword.shared"
|
||||
#if TARGET_OS_IPHONE
|
||||
additionalStoreOptions:[NSDictionary dictionaryWithObject:NSFileProtectionComplete
|
||||
forKey:NSPersistentStoreFileProtectionKey]
|
||||
#else
|
||||
additionalStoreOptions:nil
|
||||
#endif
|
||||
];
|
||||
storeManager.delegate = self;
|
||||
#ifdef DEBUG
|
||||
storeManager.hardResetEnabled = YES;
|
||||
#endif
|
||||
#if TARGET_OS_IPHONE
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification
|
||||
object:[UIApplication sharedApplication] queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
[storeManager checkiCloudStatus];
|
||||
}];
|
||||
#else
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillBecomeActiveNotification
|
||||
object:[NSApplication sharedApplication] queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
[storeManager checkiCloudStatus];
|
||||
}];
|
||||
#endif
|
||||
#if TARGET_OS_IPHONE
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillTerminateNotification
|
||||
object:[UIApplication sharedApplication] queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
[self saveContext];
|
||||
}];
|
||||
#else
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillTerminateNotification
|
||||
object:[NSApplication sharedApplication] queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
[self saveContext];
|
||||
}];
|
||||
#endif
|
||||
|
||||
return storeManager;
|
||||
}
|
||||
|
||||
- (void)saveContext {
|
||||
|
||||
[self.managedObjectContextIfReady performBlock:^{
|
||||
NSError *error = nil;
|
||||
if ([self.managedObjectContextIfReady hasChanges])
|
||||
if (![self.managedObjectContextIfReady save:&error])
|
||||
err(@"While saving context: %@", error);
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - UbiquityStoreManagerDelegate
|
||||
|
||||
- (NSManagedObjectContext *)managedObjectContextForUbiquityStoreManager:(UbiquityStoreManager *)usm {
|
||||
|
||||
return self.managedObjectContextIfReady;
|
||||
}
|
||||
|
||||
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager log:(NSString *)message {
|
||||
|
||||
dbg(@"[StoreManager] %@", message);
|
||||
}
|
||||
|
||||
- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didSwitchToiCloud:(BOOL)iCloudEnabled {
|
||||
|
||||
// manager.iCloudEnabled is more reliable (eg. iOS' MPAppDelegate tampers with didSwitch a bit)
|
||||
iCloudEnabled = manager.iCloudEnabled;
|
||||
inf(@"Using iCloud? %@", iCloudEnabled? @"YES": @"NO");
|
||||
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:iCloudEnabled? MPCheckpointCloudEnabled: MPCheckpointCloudDisabled];
|
||||
#endif
|
||||
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointCloud
|
||||
attributes:[NSDictionary dictionaryWithObject:iCloudEnabled? @"YES": @"NO"
|
||||
forKey:@"enabled"]];
|
||||
|
||||
[MPConfig get].iCloud = [NSNumber numberWithBool:iCloudEnabled];
|
||||
}
|
||||
|
||||
- (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
|
||||
[TestFlight passCheckpoint:PearlString(@"MPCheckpointMPErrorUbiquity_%d", cause)];
|
||||
#endif
|
||||
|
||||
switch (cause) {
|
||||
case UbiquityStoreManagerErrorCauseDeleteStore:
|
||||
case UbiquityStoreManagerErrorCauseDeleteLogs:
|
||||
case UbiquityStoreManagerErrorCauseCreateStorePath:
|
||||
case UbiquityStoreManagerErrorCauseClearStore:
|
||||
break;
|
||||
case UbiquityStoreManagerErrorCauseOpenLocalStore: {
|
||||
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 hardResetLocalStorage];
|
||||
|
||||
Throw(@"Local store was reset, application must be restarted to use it.");
|
||||
}
|
||||
case UbiquityStoreManagerErrorCauseOpenCloudStore: {
|
||||
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 hardResetCloudStorage];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Import / Export
|
||||
|
||||
- (MPImportResult)importSites:(NSString *)importedSitesString withPassword:(NSString *)password
|
||||
askConfirmation:(BOOL(^)(NSUInteger importCount, NSUInteger deleteCount))confirmation {
|
||||
|
||||
inf(@"Importing sites.");
|
||||
|
||||
static NSRegularExpression *headerPattern, *sitePattern;
|
||||
__block NSError *error = nil;
|
||||
if (!headerPattern) {
|
||||
headerPattern = [[NSRegularExpression alloc] initWithPattern:@"^#[[:space:]]*([^:]+): (.*)"
|
||||
options:0 error:&error];
|
||||
if (error)
|
||||
err(@"Error loading the header pattern: %@", error);
|
||||
}
|
||||
if (!sitePattern) {
|
||||
sitePattern = [[NSRegularExpression alloc] initWithPattern:@"^([^[:space:]]+)[[:space:]]+([[:digit:]]+)[[:space:]]+([[:digit:]]+)(:[[:digit:]]+)?[[:space:]]+([^\t]+)\t(.*)"
|
||||
options:0 error:&error];
|
||||
if (error)
|
||||
err(@"Error loading the site pattern: %@", error);
|
||||
}
|
||||
if (!headerPattern || !sitePattern)
|
||||
return MPImportResultInternalError;
|
||||
|
||||
MPKey *key = nil;
|
||||
__block MPUserEntity *user = nil;
|
||||
NSString *bundleVersion = nil, *keyIDHex = nil, *userName = nil;
|
||||
BOOL headerStarted = NO, headerEnded = NO, clearText = NO;
|
||||
NSArray *importedSiteLines = [importedSitesString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
|
||||
NSMutableSet *elementsToDelete = [NSMutableSet set];
|
||||
NSMutableArray *importedSiteElements = [NSMutableArray arrayWithCapacity:[importedSiteLines count]];
|
||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
||||
for (NSString *importedSiteLine in importedSiteLines) {
|
||||
if ([importedSiteLine hasPrefix:@"#"]) {
|
||||
// Comment or header
|
||||
if (!headerStarted) {
|
||||
if ([importedSiteLine isEqualToString:@"##"])
|
||||
headerStarted = YES;
|
||||
continue;
|
||||
}
|
||||
if (headerEnded)
|
||||
continue;
|
||||
if ([importedSiteLine isEqualToString:@"##"]) {
|
||||
headerEnded = YES;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Header
|
||||
if ([headerPattern numberOfMatchesInString:importedSiteLine options:0 range:NSMakeRange(0, [importedSiteLine length])] != 1) {
|
||||
err(@"Invalid header format in line: %@", importedSiteLine);
|
||||
return MPImportResultMalformedInput;
|
||||
}
|
||||
NSTextCheckingResult *headerElements = [[headerPattern matchesInString:importedSiteLine options:0
|
||||
range:NSMakeRange(0, [importedSiteLine length])] lastObject];
|
||||
NSString *headerName = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:1]];
|
||||
NSString *headerValue = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:2]];
|
||||
if ([headerName isEqualToString:@"User Name"]) {
|
||||
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;
|
||||
}
|
||||
if (!headerEnded)
|
||||
continue;
|
||||
if (!keyIDHex || ![userName length])
|
||||
return MPImportResultMalformedInput;
|
||||
if (!key) {
|
||||
key = [MPAlgorithmDefaultForBundleVersion(bundleVersion) keyForPassword:password ofUserNamed:userName];
|
||||
if (![keyIDHex isEqualToString:[key.keyID encodeHex]])
|
||||
return MPImportResultInvalidPassword;
|
||||
}
|
||||
if (![importedSiteLine length])
|
||||
continue;
|
||||
|
||||
// Site
|
||||
if ([sitePattern numberOfMatchesInString:importedSiteLine options:0 range:NSMakeRange(0, [importedSiteLine length])] != 1) {
|
||||
err(@"Invalid site format in line: %@", importedSiteLine);
|
||||
return MPImportResultMalformedInput;
|
||||
}
|
||||
NSTextCheckingResult *siteElements = [[sitePattern matchesInString:importedSiteLine options:0
|
||||
range:NSMakeRange(0, [importedSiteLine length])] lastObject];
|
||||
NSString *lastUsed = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:1]];
|
||||
NSString *uses = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:2]];
|
||||
NSString *type = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:3]];
|
||||
NSString *version = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:4]];
|
||||
NSString *name = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:5]];
|
||||
NSString *exportContent = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:6]];
|
||||
|
||||
// Find existing site.
|
||||
if (user) {
|
||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND user == %@", name, user];
|
||||
__block NSArray *existingSites = nil;
|
||||
[self.managedObjectContextIfReady performBlockAndWait:^{
|
||||
existingSites = [self.managedObjectContextIfReady executeFetchRequest:fetchRequest error:&error];
|
||||
}];
|
||||
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];
|
||||
[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.
|
||||
if (!confirmation([importedSiteElements count], [elementsToDelete count])) {
|
||||
inf(@"Import cancelled.");
|
||||
return MPImportResultCancelled;
|
||||
}
|
||||
|
||||
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
|
||||
[TestFlight passCheckpoint:MPCheckpointSitesImported];
|
||||
#endif
|
||||
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointSitesImported attributes:nil];
|
||||
|
||||
return MPImportResultSuccess;
|
||||
}
|
||||
@finally {
|
||||
[self.managedObjectContextIfReady.undoManager endUndoGrouping];
|
||||
|
||||
if (!success)
|
||||
[self.managedObjectContextIfReady.undoManager undoNestedGroup];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)exportSitesShowingPasswords:(BOOL)showPasswords {
|
||||
|
||||
inf(@"Exporting sites, %@, for: %@", showPasswords? @"showing passwords": @"omitting passwords", self.activeUser.userID);
|
||||
|
||||
// Header.
|
||||
NSMutableString *export = [NSMutableString new];
|
||||
[export appendFormat:@"# Master Password site export\n"];
|
||||
if (showPasswords)
|
||||
[export appendFormat:@"# Export of site names and passwords in clear-text.\n"];
|
||||
else
|
||||
[export appendFormat:@"# Export of site names and stored passwords (unless device-private) encrypted with the master key.\n"];
|
||||
[export appendFormat:@"# \n"];
|
||||
[export appendFormat:@"##\n"];
|
||||
[export appendFormat:@"# Version: %@\n", [PearlInfoPlist get].CFBundleVersion];
|
||||
[export appendFormat:@"# User Name: %@\n", self.activeUser.name];
|
||||
[export appendFormat:@"# Key ID: %@\n", [self.activeUser.keyID encodeHex]];
|
||||
[export appendFormat:@"# Date: %@\n", [[NSDateFormatter rfc3339DateFormatter] stringFromDate:[NSDate date]]];
|
||||
if (showPasswords)
|
||||
[export appendFormat:@"# Passwords: VISIBLE\n"];
|
||||
else
|
||||
[export appendFormat:@"# Passwords: PROTECTED\n"];
|
||||
[export appendFormat:@"##\n"];
|
||||
[export appendFormat:@"#\n"];
|
||||
[export appendFormat:@"# Last Times Password Site\tSite\n"];
|
||||
[export appendFormat:@"# used used type name\tpassword\n"];
|
||||
|
||||
// Sites.
|
||||
for (MPElementEntity *element in self.activeUser.elements) {
|
||||
NSDate *lastUsed = element.lastUsed;
|
||||
NSUInteger uses = element.uses;
|
||||
MPElementType type = element.type;
|
||||
NSUInteger version = element.version;
|
||||
NSString *name = element.name;
|
||||
NSString *content = nil;
|
||||
|
||||
// Determine the content to export.
|
||||
if (!(type & MPElementFeatureDevicePrivate)) {
|
||||
if (showPasswords)
|
||||
content = element.content;
|
||||
else
|
||||
if (type & MPElementFeatureExportContent)
|
||||
content = element.exportContent;
|
||||
}
|
||||
|
||||
[export appendFormat:@"%@ %8d %8s %20s\t%@\n",
|
||||
[[NSDateFormatter rfc3339DateFormatter] stringFromDate:lastUsed], uses,
|
||||
[PearlString(@"%u:%u", type, version) UTF8String], [name UTF8String], content
|
||||
? content: @""];
|
||||
}
|
||||
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPCheckpointSitesExported];
|
||||
#endif
|
||||
[[LocalyticsSession sharedLocalyticsSession] tagEvent:MPCheckpointSitesExported attributes:nil];
|
||||
|
||||
return export;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -6,13 +6,14 @@
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Pearl.h"
|
||||
|
||||
@interface MPConfig : PearlConfig
|
||||
|
||||
@property (nonatomic, retain) NSNumber *dataStoreError;
|
||||
@property (nonatomic, retain) NSNumber *storeKeyPhrase;
|
||||
@property (nonatomic, retain) NSNumber *rememberKeyPhrase;
|
||||
@property (nonatomic, retain) NSNumber *helpHidden;
|
||||
@property (nonatomic, retain) NSNumber *showQuickStart;
|
||||
@property (nonatomic, retain) NSNumber *rememberLogin;
|
||||
|
||||
@property (nonatomic, retain) NSNumber *iCloud;
|
||||
@property (nonatomic, retain) NSNumber *iCloudDecided;
|
||||
|
||||
+ (MPConfig *)get;
|
||||
|
||||
|
||||
@@ -6,29 +6,31 @@
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPConfig.h"
|
||||
#import "MPAppDelegate.h"
|
||||
|
||||
@implementation MPConfig
|
||||
@dynamic dataStoreError, storeKeyPhrase, rememberKeyPhrase, helpHidden, showQuickStart;
|
||||
@dynamic rememberLogin, iCloud, iCloudDecided;
|
||||
|
||||
- (id)init {
|
||||
|
||||
if(!(self = [super init]))
|
||||
return self;
|
||||
|
||||
|
||||
if (!(self = [super init]))
|
||||
return nil;
|
||||
|
||||
[self.defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(dataStoreError)),
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(storeKeyPhrase)),
|
||||
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(rememberKeyPhrase)),
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(helpHidden)),
|
||||
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(showQuickStart)),
|
||||
nil]];
|
||||
|
||||
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(askForReviews)),
|
||||
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(rememberLogin)),
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(iCloud)),
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(iCloudDecided)),
|
||||
nil]];
|
||||
|
||||
self.delegate = [MPAppDelegate get];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (MPConfig *)get {
|
||||
|
||||
|
||||
return (MPConfig *)[super get];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
@class MPUserEntity;
|
||||
|
||||
@interface MPElementEntity : NSManagedObject
|
||||
|
||||
@property (nonatomic, retain) NSString *name;
|
||||
@property (nonatomic, retain) NSString *mpHashHex;
|
||||
@property (nonatomic, assign) int16_t type;
|
||||
@property (nonatomic, assign) int16_t uses;
|
||||
@property (nonatomic, assign) NSTimeInterval lastUsed;
|
||||
@property (nonatomic, retain, readonly) id content;
|
||||
|
||||
- (void)use;
|
||||
@property (nonatomic, retain) id content;
|
||||
@property (nonatomic, retain) NSDate * lastUsed;
|
||||
@property (nonatomic, retain) NSString * name;
|
||||
@property (nonatomic, retain) NSNumber * requiresExplicitMigration_;
|
||||
@property (nonatomic, retain) NSNumber * type_;
|
||||
@property (nonatomic, retain) NSString * userName;
|
||||
@property (nonatomic, retain) NSNumber * uses_;
|
||||
@property (nonatomic, retain) NSNumber * version_;
|
||||
@property (nonatomic, retain) MPUserEntity *user;
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,42 +1,25 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
#import "MPElementEntity.h"
|
||||
#import "MPUserEntity.h"
|
||||
|
||||
|
||||
@implementation MPElementEntity
|
||||
|
||||
@dynamic name;
|
||||
@dynamic mpHashHex;
|
||||
@dynamic type;
|
||||
@dynamic uses;
|
||||
@dynamic content;
|
||||
@dynamic lastUsed;
|
||||
|
||||
- (void)use {
|
||||
|
||||
++self.uses;
|
||||
self.lastUsed = [[NSDate date] timeIntervalSinceReferenceDate];
|
||||
}
|
||||
|
||||
- (id)content {
|
||||
|
||||
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Content implementation missing." userInfo:nil];
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
|
||||
return [[self content] description];
|
||||
}
|
||||
|
||||
- (NSString *)debugDescription {
|
||||
|
||||
return [NSString stringWithFormat:@"{%@: name=%@, mpHashHex=%@, type=%d, uses=%d, lastUsed=%@}",
|
||||
NSStringFromClass([self class]), self.name, self.mpHashHex, self.type, self.uses, self.lastUsed];
|
||||
}
|
||||
@dynamic name;
|
||||
@dynamic requiresExplicitMigration_;
|
||||
@dynamic type_;
|
||||
@dynamic userName;
|
||||
@dynamic uses_;
|
||||
@dynamic version_;
|
||||
@dynamic user;
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
|
||||
@interface MPElementGeneratedEntity : MPElementEntity
|
||||
|
||||
@property (nonatomic, assign) uint16_t counter;
|
||||
@property (nonatomic, retain) NSNumber * counter_;
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,31 +1,16 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
#import "MPElementGeneratedEntity.h"
|
||||
#import "MPAppDelegate.h"
|
||||
|
||||
|
||||
@implementation MPElementGeneratedEntity
|
||||
|
||||
@dynamic counter;
|
||||
|
||||
- (id)content {
|
||||
|
||||
assert(self.type & MPElementTypeClassCalculated);
|
||||
|
||||
if (![self.name length])
|
||||
return nil;
|
||||
|
||||
if (self.type & MPElementTypeClassCalculated)
|
||||
return MPCalculateContent(self.type, self.name, [MPAppDelegate get].keyPhrase, self.counter);
|
||||
|
||||
@throw [NSException exceptionWithName:NSInternalInconsistencyException
|
||||
reason:[NSString stringWithFormat:@"Unsupported type: %d", self.type] userInfo:nil];
|
||||
}
|
||||
@dynamic counter_;
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
|
||||
@interface MPElementStoredEntity : MPElementEntity
|
||||
|
||||
@property (nonatomic, retain, readwrite) id content;
|
||||
@property (nonatomic, retain) id contentObject;
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,63 +1,16 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
#import "MPElementStoredEntity.h"
|
||||
#import "MPAppDelegate.h"
|
||||
|
||||
@interface MPElementStoredEntity ()
|
||||
|
||||
@property (nonatomic, retain, readwrite) id contentObject;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MPElementStoredEntity
|
||||
|
||||
@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 == MPElementTypeStoredDevicePrivate)
|
||||
encryptedContent = [PearlKeyChain dataOfItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]];
|
||||
else
|
||||
encryptedContent = self.contentObject;
|
||||
|
||||
NSData *decryptedContent = [encryptedContent decryptWithSymmetricKey:[[MPAppDelegate get] keyPhraseWithLength:PearlCryptKeySize]
|
||||
usePadding:YES];
|
||||
return [[NSString alloc] initWithBytes:decryptedContent.bytes length:decryptedContent.length encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
- (void)setContent:(id)content {
|
||||
|
||||
NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[[MPAppDelegate get] keyPhraseWithLength:PearlCryptKeySize]
|
||||
usePadding:YES];
|
||||
|
||||
if (self.type == MPElementTypeStoredDevicePrivate) {
|
||||
[PearlKeyChain addOrUpdateItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]
|
||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
encryptedContent, (__bridge id)kSecValueData,
|
||||
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, (__bridge id)kSecAttrAccessible,
|
||||
nil]];
|
||||
self.contentObject = nil;
|
||||
} else
|
||||
self.contentObject = encryptedContent;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
54
MasterPassword/MPEntities.h
Normal file
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
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
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// MPGuideViewController.m
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 30/01/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPGuideViewController.h"
|
||||
#import "MPAppDelegate.h"
|
||||
|
||||
@implementation MPGuideViewController
|
||||
@synthesize scrollView;
|
||||
|
||||
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
|
||||
|
||||
return (interfaceOrientation == UIInterfaceOrientationPortrait);
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
|
||||
[super viewDidLoad];
|
||||
|
||||
[PearlUIUtils autoSizeContent:self.scrollView];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
|
||||
[super viewWillDisappear:animated];
|
||||
|
||||
[MPConfig get].showQuickStart = [NSNumber numberWithBool:NO];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated {
|
||||
|
||||
[super viewDidDisappear:animated];
|
||||
|
||||
[[MPAppDelegate get] loadKeyPhrase:animated];
|
||||
}
|
||||
|
||||
|
||||
- (void)viewDidUnload {
|
||||
|
||||
[self setScrollView:nil];
|
||||
[super viewDidUnload];
|
||||
}
|
||||
|
||||
- (IBAction)close {
|
||||
|
||||
[self.presentingViewController dismissModalViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
33
MasterPassword/MPKey.h
Normal file
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
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
|
||||
@@ -1,46 +0,0 @@
|
||||
//
|
||||
// MPMainViewController.h
|
||||
// MasterPassword
|
||||
//
|
||||
// Created by Maarten Billemont on 24/11/11.
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPTypeViewController.h"
|
||||
#import "MPElementEntity.h"
|
||||
#import "MPSearchDelegate.h"
|
||||
#import "IASKAppSettingsViewController.h"
|
||||
|
||||
@interface MPMainViewController : UIViewController <MPTypeDelegate, UITextFieldDelegate, MPSearchResultsDelegate, UIWebViewDelegate, IASKSettingsDelegate>
|
||||
|
||||
@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;
|
||||
|
||||
- (IBAction)copyContent;
|
||||
- (IBAction)incrementPasswordCounter;
|
||||
- (IBAction)editPassword;
|
||||
- (IBAction)closeAlert;
|
||||
- (IBAction)action:(UIBarButtonItem *)sender;
|
||||
|
||||
- (BOOL)isHelpVisible;
|
||||
- (void)toggleHelpAnimated:(BOOL)animated;
|
||||
- (void)setHelpHidden:(BOOL)hidden animated:(BOOL)animated;
|
||||
- (void)setHelpChapter:(NSString *)chapter;
|
||||
|
||||
@end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user