Browse Source

Initial support for viewing keychain items

ray 4 years ago
parent
commit
a21e5ea158

+ 1 - 0
Classes/GlobalStateExplorers/Globals/FLEXGlobalsEntry.h

@@ -19,6 +19,7 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
     FLEXGlobalsRowCookies,
     FLEXGlobalsRowSystemLibraries,
     FLEXGlobalsRowAppClasses,
+    FLEXGlobalsRowAppKeyChainItems,
     FLEXGlobalsRowAppDelegate,
     FLEXGlobalsRowRootViewController,
     FLEXGlobalsRowUserDefaults,

+ 4 - 0
Classes/GlobalStateExplorers/Globals/FLEXGlobalsTableViewController.m

@@ -11,6 +11,7 @@
 #import "FLEXRuntimeUtility.h"
 #import "FLEXLibrariesTableViewController.h"
 #import "FLEXClassesTableViewController.h"
+#import "FLEXKeyChainTableViewController.h"
 #import "FLEXObjectExplorerViewController.h"
 #import "FLEXObjectExplorerFactory.h"
 #import "FLEXLiveObjectsTableViewController.h"
@@ -56,6 +57,8 @@ static __weak UIWindow *s_applicationWindow = nil;
     switch (row) {
         case FLEXGlobalsRowAppClasses:
             return [FLEXClassesTableViewController flex_concreteGlobalsEntry:row];
+        case FLEXGlobalsRowAppKeyChainItems:
+            return [FLEXKeyChainTableViewController flex_concreteGlobalsEntry:row];
         case FLEXGlobalsRowAddressInspector:
             return [FLEXAddressExplorerCoordinator flex_concreteGlobalsEntry:row];
         case FLEXGlobalsRowSystemLibraries:
@@ -121,6 +124,7 @@ static __weak UIWindow *s_applicationWindow = nil;
             @[ // FLEXGlobalsSectionAppShortcuts
                 [self globalsEntryForRow:FLEXGlobalsRowMainBundle],
                 [self globalsEntryForRow:FLEXGlobalsRowUserDefaults],
+                [self globalsEntryForRow:FLEXGlobalsRowAppKeyChainItems],
                 [self globalsEntryForRow:FLEXGlobalsRowApplication],
                 [self globalsEntryForRow:FLEXGlobalsRowAppDelegate],
                 [self globalsEntryForRow:FLEXGlobalsRowKeyWindow],

+ 18 - 0
Classes/GlobalStateExplorers/KeyChain/FLEXKeyChainTableViewController.h

@@ -0,0 +1,18 @@
+//
+//  FLEXKeyChainTableViewController.h
+//  FLEX
+//
+//  Created by ray on 2019/8/17.
+//  Copyright © 2019 Flipboard. All rights reserved.
+//
+
+#import "FLEXGlobalsEntry.h"
+#import "FLEXTableViewController.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FLEXKeyChainTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 137 - 0
Classes/GlobalStateExplorers/KeyChain/FLEXKeyChainTableViewController.m

@@ -0,0 +1,137 @@
+//
+//  FLEXKeyChainTableViewController.m
+//  FLEX
+//
+//  Created by ray on 2019/8/17.
+//  Copyright © 2019 Flipboard. All rights reserved.
+//
+
+#import "FLEXKeychain.h"
+#import "FLEXKeychainQuery.h"
+#import "FLEXKeyChainTableViewController.h"
+#import "FLEXUtility.h"
+
+@interface FLEXKeyChainTableViewController ()
+
+@property (nonatomic) NSArray<NSDictionary *> *keyChainItems;
+
+@end
+
+@implementation FLEXKeyChainTableViewController
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    
+    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(clearKeyChain)];
+
+    
+    _keyChainItems = [FLEXKeychain allAccounts];
+    self.title = [NSString stringWithFormat:@"🔑 KeyChains (%lu)", (unsigned long)self.keyChainItems.count];
+}
+
+- (void)clearKeyChain
+{
+    
+    for (id account in _keyChainItems) {
+
+        FLEXKeychainQuery *query = [[FLEXKeychainQuery alloc] init];
+
+        query.service = [account valueForKey:kSSKeychainWhereKey];
+        query.account = [account valueForKey:kSSKeychainAccountKey];
+        
+        if(![query deleteItem:nil]) {
+            NSLog(@"Delete Keychin Item Failed.");
+        }
+    }
+    
+    _keyChainItems = [FLEXKeychain allAccounts];
+    [self.tableView reloadData];
+}
+
+#pragma mark - FLEXGlobalsEntry
+
++ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row
+{
+    return [NSString stringWithFormat:@"🔑  %@ KeyChain", [FLEXUtility applicationName]];
+}
+
++ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row
+{
+    FLEXKeyChainTableViewController *keyChainViewController = [self new];
+    
+    return keyChainViewController;
+}
+
+
+#pragma mark - Table View Data Source
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
+{
+    return 1;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+    return self.keyChainItems.count;
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    static NSString *CellIdentifier = @"Cell";
+    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
+    if (!cell) {
+        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
+        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
+        cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
+    }
+    
+    NSDictionary *item = self.keyChainItems[indexPath.row];
+    cell.textLabel.text = item[kSSKeychainAccountKey];
+    
+    return cell;
+}
+
+
+#pragma mark - Table View Delegate
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    NSDictionary *item = self.keyChainItems[indexPath.row];
+    
+    FLEXKeychainQuery *query = [[FLEXKeychainQuery alloc] init];
+    query.service = [item valueForKey:kSSKeychainWhereKey];
+    query.account = [item valueForKey:kSSKeychainAccountKey];
+    [query fetch:nil];
+    
+    NSString *msg = nil;
+    
+    if ([query.password length])
+    {
+        msg = query.password;
+    }
+    
+    else if ([query.passwordData length])
+    {
+        msg = [query.passwordData description];
+    }
+    
+    else
+    {
+        msg = @"NO Data!";
+    }
+    
+    UIAlertController *cv = [UIAlertController alertControllerWithTitle:@"Password" message:msg preferredStyle:UIAlertControllerStyleAlert];
+    [cv addAction:[UIAlertAction actionWithTitle:@"Dismiss" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
+        [cv dismissViewControllerAnimated:YES completion:nil];
+    }]];
+    
+    [cv addAction:[UIAlertAction actionWithTitle:@"Copy" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
+        UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
+        pasteboard.string = msg;
+    }]];
+    
+    [self presentViewController:cv animated:YES completion:nil];
+}
+
+@end

+ 198 - 0
Classes/GlobalStateExplorers/KeyChain/FLEXKeychain.h

@@ -0,0 +1,198 @@
+//
+//  SSKeychain.h
+//  SSKeychain
+//
+//  Created by Sam Soffes on 5/19/10.
+//  Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
+//
+
+#if __has_feature(modules)
+	@import Foundation;
+#else
+	#import <Foundation/Foundation.h>
+#endif
+
+/**
+ Error code specific to SSKeychain that can be returned in NSError objects.
+ For codes returned by the operating system, refer to SecBase.h for your
+ platform.
+ */
+typedef NS_ENUM(OSStatus, SSKeychainErrorCode) {
+	/** Some of the arguments were invalid. */
+	SSKeychainErrorBadArguments = -1001,
+};
+
+/** SSKeychain error domain */
+extern NSString *const kSSKeychainErrorDomain;
+
+/** Account name. */
+extern NSString *const kSSKeychainAccountKey;
+
+/**
+ Time the item was created.
+
+ The value will be a string.
+ */
+extern NSString *const kSSKeychainCreatedAtKey;
+
+/** Item class. */
+extern NSString *const kSSKeychainClassKey;
+
+/** Item description. */
+extern NSString *const kSSKeychainDescriptionKey;
+
+/** Item label. */
+extern NSString *const kSSKeychainLabelKey;
+
+/** Time the item was last modified.
+
+ The value will be a string.
+ */
+extern NSString *const kSSKeychainLastModifiedKey;
+
+/** Where the item was created. */
+extern NSString *const kSSKeychainWhereKey;
+
+/**
+ Simple wrapper for accessing accounts, getting passwords, setting passwords, and deleting passwords using the system
+ Keychain on Mac OS X and iOS.
+
+ This was originally inspired by EMKeychain and SDKeychain (both of which are now gone). Thanks to the authors.
+ SSKeychain has since switched to a simpler implementation that was abstracted from [SSToolkit](http://sstoolk.it).
+ */
+@interface FLEXKeychain : NSObject
+
+#pragma mark - Classic methods
+
+/**
+ Returns a string containing the password for a given account and service, or `nil` if the Keychain doesn't have a
+ password for the given parameters.
+
+ @param serviceName The service for which to return the corresponding password.
+
+ @param account The account for which to return the corresponding password.
+
+ @return Returns a string containing the password for a given account and service, or `nil` if the Keychain doesn't
+ have a password for the given parameters.
+ */
++ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account;
++ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error __attribute__((swift_error(none)));
+
+/**
+ Returns a nsdata containing the password for a given account and service, or `nil` if the Keychain doesn't have a
+ password for the given parameters.
+
+ @param serviceName The service for which to return the corresponding password.
+
+ @param account The account for which to return the corresponding password.
+
+ @return Returns a nsdata containing the password for a given account and service, or `nil` if the Keychain doesn't
+ have a password for the given parameters.
+ */
++ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account;
++ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error __attribute__((swift_error(none)));
+
+
+/**
+ Deletes a password from the Keychain.
+
+ @param serviceName The service for which to delete the corresponding password.
+
+ @param account The account for which to delete the corresponding password.
+
+ @return Returns `YES` on success, or `NO` on failure.
+ */
++ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account;
++ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error __attribute__((swift_error(none)));
+
+
+/**
+ Sets a password in the Keychain.
+
+ @param password The password to store in the Keychain.
+
+ @param serviceName The service for which to set the corresponding password.
+
+ @param account The account for which to set the corresponding password.
+
+ @return Returns `YES` on success, or `NO` on failure.
+ */
++ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account;
++ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error __attribute__((swift_error(none)));
+
+/**
+ Sets a password in the Keychain.
+
+ @param password The password to store in the Keychain.
+
+ @param serviceName The service for which to set the corresponding password.
+
+ @param account The account for which to set the corresponding password.
+
+ @return Returns `YES` on success, or `NO` on failure.
+ */
++ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account;
++ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error __attribute__((swift_error(none)));
+
+/**
+ Returns an array containing the Keychain's accounts, or `nil` if the Keychain has no accounts.
+
+ See the `NSString` constants declared in SSKeychain.h for a list of keys that can be used when accessing the
+ dictionaries returned by this method.
+
+ @return An array of dictionaries containing the Keychain's accounts, or `nil` if the Keychain doesn't have any
+ accounts. The order of the objects in the array isn't defined.
+ */
++ (NSArray<NSDictionary<NSString *, id> *> *)allAccounts;
++ (NSArray<NSDictionary<NSString *, id> *> *)allAccounts:(NSError *__autoreleasing *)error __attribute__((swift_error(none)));
+
+
+/**
+ Returns an array containing the Keychain's accounts for a given service, or `nil` if the Keychain doesn't have any
+ accounts for the given service.
+
+ See the `NSString` constants declared in SSKeychain.h for a list of keys that can be used when accessing the
+ dictionaries returned by this method.
+
+ @param serviceName The service for which to return the corresponding accounts.
+
+ @return An array of dictionaries containing the Keychain's accounts for a given `serviceName`, or `nil` if the Keychain
+ doesn't have any accounts for the given `serviceName`. The order of the objects in the array isn't defined.
+ */
++ (NSArray<NSDictionary<NSString *, id> *> *)accountsForService:(NSString *)serviceName;
++ (NSArray<NSDictionary<NSString *, id> *> *)accountsForService:(NSString *)serviceName error:(NSError *__autoreleasing *)error __attribute__((swift_error(none)));
+
+
+#pragma mark - Configuration
+
+#if __IPHONE_4_0 && TARGET_OS_IPHONE
+/**
+ Returns the accessibility type for all future passwords saved to the Keychain.
+
+ @return Returns the accessibility type.
+
+ The return value will be `NULL` or one of the "Keychain Item Accessibility
+ Constants" used for determining when a keychain item should be readable.
+
+ @see setAccessibilityType
+ */
++ (CFTypeRef)accessibilityType;
+
+/**
+ Sets the accessibility type for all future passwords saved to the Keychain.
+
+ @param accessibilityType One of the "Keychain Item Accessibility Constants"
+ used for determining when a keychain item should be readable.
+
+ If the value is `NULL` (the default), the Keychain default will be used which
+ is highly insecure. You really should use at least `kSecAttrAccessibleAfterFirstUnlock`
+ for background applications or `kSecAttrAccessibleWhenUnlocked` for all
+ other applications.
+
+ @see accessibilityType
+ */
++ (void)setAccessibilityType:(CFTypeRef)accessibilityType;
+#endif
+
+@end
+

+ 130 - 0
Classes/GlobalStateExplorers/KeyChain/FLEXKeychain.m

@@ -0,0 +1,130 @@
+//
+//  SSKeychain.m
+//  SSKeychain
+//
+//  Created by Sam Soffes on 5/19/10.
+//  Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
+//
+
+#import "FLEXKeychain.h"
+#import "FLEXKeychainQuery.h"
+
+NSString *const kSSKeychainErrorDomain = @"com.samsoffes.sskeychain";
+NSString *const kSSKeychainAccountKey = @"acct";
+NSString *const kSSKeychainCreatedAtKey = @"cdat";
+NSString *const kSSKeychainClassKey = @"labl";
+NSString *const kSSKeychainDescriptionKey = @"desc";
+NSString *const kSSKeychainLabelKey = @"labl";
+NSString *const kSSKeychainLastModifiedKey = @"mdat";
+NSString *const kSSKeychainWhereKey = @"svce";
+
+#if __IPHONE_4_0 && TARGET_OS_IPHONE
+	static CFTypeRef SSKeychainAccessibilityType = NULL;
+#endif
+
+@implementation FLEXKeychain
+
++ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account {
+	return [self passwordForService:serviceName account:account error:nil];
+}
+
+
++ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
+	FLEXKeychainQuery *query = [[FLEXKeychainQuery alloc] init];
+	query.service = serviceName;
+	query.account = account;
+	[query fetch:error];
+	return query.password;
+}
+
++ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account {
+	return [self passwordDataForService:serviceName account:account error:nil];
+}
+
++ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error {
+    FLEXKeychainQuery *query = [[FLEXKeychainQuery alloc] init];
+    query.service = serviceName;
+    query.account = account;
+    [query fetch:error];
+
+    return query.passwordData;
+}
+
+
++ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account {
+	return [self deletePasswordForService:serviceName account:account error:nil];
+}
+
+
++ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
+	FLEXKeychainQuery *query = [[FLEXKeychainQuery alloc] init];
+	query.service = serviceName;
+	query.account = account;
+	return [query deleteItem:error];
+}
+
+
++ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account {
+	return [self setPassword:password forService:serviceName account:account error:nil];
+}
+
+
++ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
+	FLEXKeychainQuery *query = [[FLEXKeychainQuery alloc] init];
+	query.service = serviceName;
+	query.account = account;
+	query.password = password;
+	return [query save:error];
+}
+
++ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account {
+	return [self setPasswordData:password forService:serviceName account:account error:nil];
+}
+
+
++ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error {
+    FLEXKeychainQuery *query = [[FLEXKeychainQuery alloc] init];
+    query.service = serviceName;
+    query.account = account;
+    query.passwordData = password;
+    return [query save:error];
+}
+
++ (NSArray *)allAccounts {
+	return [self allAccounts:nil];
+}
+
+
++ (NSArray *)allAccounts:(NSError *__autoreleasing *)error {
+    return [self accountsForService:nil error:error];
+}
+
+
++ (NSArray *)accountsForService:(NSString *)serviceName {
+	return [self accountsForService:serviceName error:nil];
+}
+
+
++ (NSArray *)accountsForService:(NSString *)serviceName error:(NSError *__autoreleasing *)error {
+    FLEXKeychainQuery *query = [[FLEXKeychainQuery alloc] init];
+    query.service = serviceName;
+    return [query fetchAll:error];
+}
+
+
+#if __IPHONE_4_0 && TARGET_OS_IPHONE
++ (CFTypeRef)accessibilityType {
+	return SSKeychainAccessibilityType;
+}
+
+
++ (void)setAccessibilityType:(CFTypeRef)accessibilityType {
+	CFRetain(accessibilityType);
+	if (SSKeychainAccessibilityType) {
+		CFRelease(SSKeychainAccessibilityType);
+	}
+	SSKeychainAccessibilityType = accessibilityType;
+}
+#endif
+
+@end

+ 143 - 0
Classes/GlobalStateExplorers/KeyChain/FLEXKeychainQuery.h

@@ -0,0 +1,143 @@
+//
+//  SSKeychainQuery.h
+//  SSKeychain
+//
+//  Created by Caleb Davenport on 3/19/13.
+//  Copyright (c) 2013-2014 Sam Soffes. All rights reserved.
+//
+
+#if __has_feature(modules)
+	@import Foundation;
+	@import Security;
+#else
+	#import <Foundation/Foundation.h>
+	#import <Security/Security.h>
+#endif
+
+#if __IPHONE_7_0 || __MAC_10_9
+	// Keychain synchronization available at compile time
+	#define SSKEYCHAIN_SYNCHRONIZATION_AVAILABLE 1
+#endif
+
+#if __IPHONE_3_0 || __MAC_10_9
+	// Keychain access group available at compile time
+	#define SSKEYCHAIN_ACCESS_GROUP_AVAILABLE 1
+#endif
+
+#ifdef SSKEYCHAIN_SYNCHRONIZATION_AVAILABLE
+typedef NS_ENUM(NSUInteger, SSKeychainQuerySynchronizationMode) {
+	SSKeychainQuerySynchronizationModeAny,
+	SSKeychainQuerySynchronizationModeNo,
+	SSKeychainQuerySynchronizationModeYes
+};
+#endif
+
+/**
+ Simple interface for querying or modifying keychain items.
+ */
+@interface FLEXKeychainQuery : NSObject
+
+/** kSecAttrAccount */
+@property (nonatomic, copy) NSString *account;
+
+/** kSecAttrService */
+@property (nonatomic, copy) NSString *service;
+
+/** kSecAttrLabel */
+@property (nonatomic, copy) NSString *label;
+
+#ifdef SSKEYCHAIN_ACCESS_GROUP_AVAILABLE
+/** kSecAttrAccessGroup (only used on iOS) */
+@property (nonatomic, copy) NSString *accessGroup;
+#endif
+
+#ifdef SSKEYCHAIN_SYNCHRONIZATION_AVAILABLE
+/** kSecAttrSynchronizable */
+@property (nonatomic) SSKeychainQuerySynchronizationMode synchronizationMode;
+#endif
+
+/** Root storage for password information */
+@property (nonatomic, copy) NSData *passwordData;
+
+/**
+ This property automatically transitions between an object and the value of
+ `passwordData` using NSKeyedArchiver and NSKeyedUnarchiver.
+ */
+@property (nonatomic, copy) id<NSCoding> passwordObject;
+
+/**
+ Convenience accessor for setting and getting a password string. Passes through
+ to `passwordData` using UTF-8 string encoding.
+ */
+@property (nonatomic, copy) NSString *password;
+
+
+///------------------------
+/// @name Saving & Deleting
+///------------------------
+
+/**
+ Save the receiver's attributes as a keychain item. Existing items with the
+ given account, service, and access group will first be deleted.
+
+ @param error Populated should an error occur.
+
+ @return `YES` if saving was successful, `NO` otherwise.
+ */
+- (BOOL)save:(NSError **)error;
+
+/**
+ Delete keychain items that match the given account, service, and access group.
+
+ @param error Populated should an error occur.
+
+ @return `YES` if saving was successful, `NO` otherwise.
+ */
+- (BOOL)deleteItem:(NSError **)error;
+
+
+///---------------
+/// @name Fetching
+///---------------
+
+/**
+ Fetch all keychain items that match the given account, service, and access
+ group. The values of `password` and `passwordData` are ignored when fetching.
+
+ @param error Populated should an error occur.
+
+ @return An array of dictionaries that represent all matching keychain items or
+ `nil` should an error occur.
+ The order of the items is not determined.
+ */
+- (NSArray<NSDictionary<NSString *, id> *> *)fetchAll:(NSError **)error;
+
+/**
+ Fetch the keychain item that matches the given account, service, and access
+ group. The `password` and `passwordData` properties will be populated unless
+ an error occurs. The values of `password` and `passwordData` are ignored when
+ fetching.
+
+ @param error Populated should an error occur.
+
+ @return `YES` if fetching was successful, `NO` otherwise.
+ */
+- (BOOL)fetch:(NSError **)error;
+
+
+///-----------------------------
+/// @name Synchronization Status
+///-----------------------------
+
+#ifdef SSKEYCHAIN_SYNCHRONIZATION_AVAILABLE
+/**
+ Returns a boolean indicating if keychain synchronization is available on the device at runtime. The #define 
+ SSKEYCHAIN_SYNCHRONIZATION_AVAILABLE is only for compile time. If you are checking for the presence of synchronization,
+ you should use this method.
+ 
+ @return A value indicating if keychain synchronization is available
+ */
++ (BOOL)isSynchronizationAvailable;
+#endif
+
+@end

+ 316 - 0
Classes/GlobalStateExplorers/KeyChain/FLEXKeychainQuery.m

@@ -0,0 +1,316 @@
+//
+//  SSKeychainQuery.m
+//  SSKeychain
+//
+//  Created by Caleb Davenport on 3/19/13.
+//  Copyright (c) 2013-2014 Sam Soffes. All rights reserved.
+//
+
+#import "FLEXKeychainQuery.h"
+#import "FLEXKeychain.h"
+
+@implementation FLEXKeychainQuery
+
+@synthesize account = _account;
+@synthesize service = _service;
+@synthesize label = _label;
+@synthesize passwordData = _passwordData;
+
+#ifdef SSKEYCHAIN_ACCESS_GROUP_AVAILABLE
+@synthesize accessGroup = _accessGroup;
+#endif
+
+#ifdef SSKEYCHAIN_SYNCHRONIZATION_AVAILABLE
+@synthesize synchronizationMode = _synchronizationMode;
+#endif
+
+#pragma mark - Public
+
+- (BOOL)save:(NSError *__autoreleasing *)error {
+	OSStatus status = SSKeychainErrorBadArguments;
+	if (!self.service || !self.account || !self.passwordData) {
+		if (error) {
+			*error = [[self class] errorWithCode:status];
+		}
+		return NO;
+	}
+	NSMutableDictionary *query = nil;
+	NSMutableDictionary * searchQuery = [self query];
+	status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, nil);
+	if (status == errSecSuccess) {//item already exists, update it!
+		query = [[NSMutableDictionary alloc]init];
+		[query setObject:self.passwordData forKey:(__bridge id)kSecValueData];
+#if __IPHONE_4_0 && TARGET_OS_IPHONE
+		CFTypeRef accessibilityType = [FLEXKeychain accessibilityType];
+		if (accessibilityType) {
+			[query setObject:(__bridge id)accessibilityType forKey:(__bridge id)kSecAttrAccessible];
+		}
+#endif
+		status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(query));
+	}else if(status == errSecItemNotFound){//item not found, create it!
+		query = [self query];
+		if (self.label) {
+			[query setObject:self.label forKey:(__bridge id)kSecAttrLabel];
+		}
+		[query setObject:self.passwordData forKey:(__bridge id)kSecValueData];
+#if __IPHONE_4_0 && TARGET_OS_IPHONE
+		CFTypeRef accessibilityType = [FLEXKeychain accessibilityType];
+		if (accessibilityType) {
+			[query setObject:(__bridge id)accessibilityType forKey:(__bridge id)kSecAttrAccessible];
+		}
+#endif
+		status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
+	}
+	if (status != errSecSuccess && error != NULL) {
+		*error = [[self class] errorWithCode:status];
+	}
+	return (status == errSecSuccess);}
+
+
+- (BOOL)deleteItem:(NSError *__autoreleasing *)error {
+	OSStatus status = SSKeychainErrorBadArguments;
+	if (!self.service || !self.account) {
+		if (error) {
+			*error = [[self class] errorWithCode:status];
+		}
+		return NO;
+	}
+
+	NSMutableDictionary *query = [self query];
+#if TARGET_OS_IPHONE
+	status = SecItemDelete((__bridge CFDictionaryRef)query);
+#else
+	// On Mac OS, SecItemDelete will not delete a key created in a different
+	// app, nor in a different version of the same app.
+	//
+	// To replicate the issue, save a password, change to the code and
+	// rebuild the app, and then attempt to delete that password.
+	//
+	// This was true in OS X 10.6 and probably later versions as well.
+	//
+	// Work around it by using SecItemCopyMatching and SecKeychainItemDelete.
+	CFTypeRef result = NULL;
+	[query setObject:@YES forKey:(__bridge id)kSecReturnRef];
+	status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
+	if (status == errSecSuccess) {
+		status = SecKeychainItemDelete((SecKeychainItemRef)result);
+		CFRelease(result);
+	}
+#endif
+
+	if (status != errSecSuccess && error != NULL) {
+		*error = [[self class] errorWithCode:status];
+	}
+
+	return (status == errSecSuccess);
+}
+
+
+- (NSArray *)fetchAll:(NSError *__autoreleasing *)error {
+	NSMutableDictionary *query = [self query];
+	[query setObject:@YES forKey:(__bridge id)kSecReturnAttributes];
+	[query setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];
+#if __IPHONE_4_0 && TARGET_OS_IPHONE
+	CFTypeRef accessibilityType = [FLEXKeychain accessibilityType];
+	if (accessibilityType) {
+		[query setObject:(__bridge id)accessibilityType forKey:(__bridge id)kSecAttrAccessible];
+	}
+#endif
+
+	CFTypeRef result = NULL;
+	OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
+	if (status != errSecSuccess && error != NULL) {
+		*error = [[self class] errorWithCode:status];
+		return nil;
+	}
+
+	return (__bridge_transfer NSArray *)result;
+}
+
+
+- (BOOL)fetch:(NSError *__autoreleasing *)error {
+	OSStatus status = SSKeychainErrorBadArguments;
+	if (!self.service || !self.account) {
+		if (error) {
+			*error = [[self class] errorWithCode:status];
+		}
+		return NO;
+	}
+
+	CFTypeRef result = NULL;
+	NSMutableDictionary *query = [self query];
+	[query setObject:@YES forKey:(__bridge id)kSecReturnData];
+	[query setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
+	status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
+
+	if (status != errSecSuccess) {
+		if (error) {
+			*error = [[self class] errorWithCode:status];
+		}
+		return NO;
+	}
+
+	self.passwordData = (__bridge_transfer NSData *)result;
+	return YES;
+}
+
+
+#pragma mark - Accessors
+
+- (void)setPasswordObject:(id<NSCoding>)object {
+	self.passwordData = [NSKeyedArchiver archivedDataWithRootObject:object];
+}
+
+
+- (id<NSCoding>)passwordObject {
+	if ([self.passwordData length]) {
+		return [NSKeyedUnarchiver unarchiveObjectWithData:self.passwordData];
+	}
+	return nil;
+}
+
+
+- (void)setPassword:(NSString *)password {
+	self.passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
+}
+
+
+- (NSString *)password {
+	if ([self.passwordData length]) {
+		return [[NSString alloc] initWithData:self.passwordData encoding:NSUTF8StringEncoding];
+	}
+	return nil;
+}
+
+
+#pragma mark - Synchronization Status
+
+#ifdef SSKEYCHAIN_SYNCHRONIZATION_AVAILABLE
++ (BOOL)isSynchronizationAvailable {
+#if TARGET_OS_IPHONE
+	// Apple suggested way to check for 7.0 at runtime
+	// https://developer.apple.com/library/ios/documentation/userexperience/conceptual/transitionguide/SupportingEarlieriOS.html
+	return floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1;
+#else
+	return floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_8_4;
+#endif
+}
+#endif
+
+
+#pragma mark - Private
+
+- (NSMutableDictionary *)query {
+	NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:3];
+	[dictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
+
+	if (self.service) {
+		[dictionary setObject:self.service forKey:(__bridge id)kSecAttrService];
+	}
+
+	if (self.account) {
+		[dictionary setObject:self.account forKey:(__bridge id)kSecAttrAccount];
+	}
+
+#ifdef SSKEYCHAIN_ACCESS_GROUP_AVAILABLE
+#if !TARGET_IPHONE_SIMULATOR
+	if (self.accessGroup) {
+		[dictionary setObject:self.accessGroup forKey:(__bridge id)kSecAttrAccessGroup];
+	}
+#endif
+#endif
+
+#ifdef SSKEYCHAIN_SYNCHRONIZATION_AVAILABLE
+	if ([[self class] isSynchronizationAvailable]) {
+		id value;
+
+		switch (self.synchronizationMode) {
+			case SSKeychainQuerySynchronizationModeNo: {
+			  value = @NO;
+			  break;
+			}
+			case SSKeychainQuerySynchronizationModeYes: {
+			  value = @YES;
+			  break;
+			}
+			case SSKeychainQuerySynchronizationModeAny: {
+			  value = (__bridge id)(kSecAttrSynchronizableAny);
+			  break;
+			}
+		}
+
+		[dictionary setObject:value forKey:(__bridge id)(kSecAttrSynchronizable)];
+	}
+#endif
+
+	return dictionary;
+}
+
+
++ (NSError *)errorWithCode:(OSStatus) code {
+	static dispatch_once_t onceToken;
+	static NSBundle *resourcesBundle = nil;
+	dispatch_once(&onceToken, ^{
+		NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"SSKeychain" withExtension:@"bundle"];
+		resourcesBundle = [NSBundle bundleWithURL:url];
+	});
+	
+	NSString *message = nil;
+	switch (code) {
+		case errSecSuccess: return nil;
+		case SSKeychainErrorBadArguments: message = NSLocalizedStringFromTableInBundle(@"SSKeychainErrorBadArguments", @"SSKeychain", resourcesBundle, nil); break;
+
+#if TARGET_OS_IPHONE
+		case errSecUnimplemented: {
+			message = NSLocalizedStringFromTableInBundle(@"errSecUnimplemented", @"SSKeychain", resourcesBundle, nil);
+			break;
+		}
+		case errSecParam: {
+			message = NSLocalizedStringFromTableInBundle(@"errSecParam", @"SSKeychain", resourcesBundle, nil);
+			break;
+		}
+		case errSecAllocate: {
+			message = NSLocalizedStringFromTableInBundle(@"errSecAllocate", @"SSKeychain", resourcesBundle, nil);
+			break;
+		}
+		case errSecNotAvailable: {
+			message = NSLocalizedStringFromTableInBundle(@"errSecNotAvailable", @"SSKeychain", resourcesBundle, nil);
+			break;
+		}
+		case errSecDuplicateItem: {
+			message = NSLocalizedStringFromTableInBundle(@"errSecDuplicateItem", @"SSKeychain", resourcesBundle, nil);
+			break;
+		}
+		case errSecItemNotFound: {
+			message = NSLocalizedStringFromTableInBundle(@"errSecItemNotFound", @"SSKeychain", resourcesBundle, nil);
+			break;
+		}
+		case errSecInteractionNotAllowed: {
+			message = NSLocalizedStringFromTableInBundle(@"errSecInteractionNotAllowed", @"SSKeychain", resourcesBundle, nil);
+			break;
+		}
+		case errSecDecode: {
+			message = NSLocalizedStringFromTableInBundle(@"errSecDecode", @"SSKeychain", resourcesBundle, nil);
+			break;
+		}
+		case errSecAuthFailed: {
+			message = NSLocalizedStringFromTableInBundle(@"errSecAuthFailed", @"SSKeychain", resourcesBundle, nil);
+			break;
+		}
+		default: {
+			message = NSLocalizedStringFromTableInBundle(@"errSecDefault", @"SSKeychain", resourcesBundle, nil);
+		}
+#else
+		default:
+			message = (__bridge_transfer NSString *)SecCopyErrorMessageString(code, NULL);
+#endif
+	}
+
+	NSDictionary *userInfo = nil;
+	if (message) {
+		userInfo = @{ NSLocalizedDescriptionKey : message };
+	}
+	return [NSError errorWithDomain:kSSKeychainErrorDomain code:code userInfo:userInfo];
+}
+
+@end

+ 1 - 1
FLEX.podspec

@@ -32,7 +32,7 @@ Pod::Spec.new do |spec|
   spec.platform         = :ios, "9.0"
   spec.source           = { :git => "https://github.com/Flipboard/FLEX.git", :tag => "#{spec.version}" }
   spec.source_files     = "Classes/**/*.{h,m,mm}"
-  spec.frameworks       = [ "Foundation", "UIKit", "CoreGraphics", "ImageIO", "QuartzCore", "WebKit" ]
+  spec.frameworks       = [ "Foundation", "UIKit", "CoreGraphics", "ImageIO", "QuartzCore", "WebKit", "Security" ]
   spec.libraries        = [ "z", "sqlite3" ]
   spec.requires_arc     = true
   spec.public_header_files = [ "Classes/**/FLEXManager.h", "Classes/FLEX.h" ]

+ 32 - 0
FLEX.xcodeproj/project.pbxproj

@@ -133,6 +133,12 @@
 		3A4C95471B5B217D0088C3F2 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A4C95461B5B217D0088C3F2 /* libz.dylib */; };
 		679F64861BD53B7B00A8C94C /* FLEXCookiesTableViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 679F64841BD53B7B00A8C94C /* FLEXCookiesTableViewController.h */; };
 		679F64871BD53B7B00A8C94C /* FLEXCookiesTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 679F64851BD53B7B00A8C94C /* FLEXCookiesTableViewController.m */; };
+		71E1C2132307FBB800F5032A /* FLEXKeychain.h in Headers */ = {isa = PBXBuildFile; fileRef = 71E1C20B2307FBB700F5032A /* FLEXKeychain.h */; };
+		71E1C2142307FBB800F5032A /* FLEXKeyChainTableViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 71E1C20C2307FBB700F5032A /* FLEXKeyChainTableViewController.h */; };
+		71E1C2152307FBB800F5032A /* FLEXKeychainQuery.h in Headers */ = {isa = PBXBuildFile; fileRef = 71E1C20D2307FBB700F5032A /* FLEXKeychainQuery.h */; };
+		71E1C2172307FBB800F5032A /* FLEXKeychain.m in Sources */ = {isa = PBXBuildFile; fileRef = 71E1C20F2307FBB700F5032A /* FLEXKeychain.m */; };
+		71E1C2182307FBB800F5032A /* FLEXKeyChainTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 71E1C2102307FBB700F5032A /* FLEXKeyChainTableViewController.m */; };
+		71E1C2192307FBB800F5032A /* FLEXKeychainQuery.m in Sources */ = {isa = PBXBuildFile; fileRef = 71E1C2112307FBB700F5032A /* FLEXKeychainQuery.m */; };
 		7349FD6A22B93CDF00051810 /* FLEXColor.h in Headers */ = {isa = PBXBuildFile; fileRef = 7349FD6822B93CDF00051810 /* FLEXColor.h */; };
 		7349FD6B22B93CDF00051810 /* FLEXColor.m in Sources */ = {isa = PBXBuildFile; fileRef = 7349FD6922B93CDF00051810 /* FLEXColor.m */; };
 		779B1ECE1C0C4D7C001F5E49 /* FLEXDatabaseManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 779B1EC01C0C4D7C001F5E49 /* FLEXDatabaseManager.h */; };
@@ -346,6 +352,12 @@
 		3A4C95461B5B217D0088C3F2 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; };
 		679F64841BD53B7B00A8C94C /* FLEXCookiesTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXCookiesTableViewController.h; sourceTree = "<group>"; };
 		679F64851BD53B7B00A8C94C /* FLEXCookiesTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXCookiesTableViewController.m; sourceTree = "<group>"; };
+		71E1C20B2307FBB700F5032A /* FLEXKeychain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXKeychain.h; sourceTree = "<group>"; };
+		71E1C20C2307FBB700F5032A /* FLEXKeyChainTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXKeyChainTableViewController.h; sourceTree = "<group>"; };
+		71E1C20D2307FBB700F5032A /* FLEXKeychainQuery.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXKeychainQuery.h; sourceTree = "<group>"; };
+		71E1C20F2307FBB700F5032A /* FLEXKeychain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXKeychain.m; sourceTree = "<group>"; };
+		71E1C2102307FBB700F5032A /* FLEXKeyChainTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXKeyChainTableViewController.m; sourceTree = "<group>"; };
+		71E1C2112307FBB700F5032A /* FLEXKeychainQuery.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXKeychainQuery.m; sourceTree = "<group>"; };
 		7349FD6822B93CDF00051810 /* FLEXColor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXColor.h; sourceTree = "<group>"; };
 		7349FD6922B93CDF00051810 /* FLEXColor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXColor.m; sourceTree = "<group>"; };
 		779B1EC01C0C4D7C001F5E49 /* FLEXDatabaseManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXDatabaseManager.h; sourceTree = "<group>"; };
@@ -616,6 +628,7 @@
 		3A4C949A1B5B21410088C3F2 /* GlobalStateExplorers */ = {
 			isa = PBXGroup;
 			children = (
+				71E1C2092307FBB700F5032A /* KeyChain */,
 				C3511B8E22D7C9740057BAB7 /* Globals */,
 				779B1EBF1C0C4D7C001F5E49 /* DatabaseBrowser */,
 				3A4C94AD1B5B21410088C3F2 /* SystemLog */,
@@ -700,6 +713,19 @@
 			name = Frameworks;
 			sourceTree = "<group>";
 		};
+		71E1C2092307FBB700F5032A /* KeyChain */ = {
+			isa = PBXGroup;
+			children = (
+				71E1C20B2307FBB700F5032A /* FLEXKeychain.h */,
+				71E1C20F2307FBB700F5032A /* FLEXKeychain.m */,
+				71E1C20D2307FBB700F5032A /* FLEXKeychainQuery.h */,
+				71E1C2112307FBB700F5032A /* FLEXKeychainQuery.m */,
+				71E1C20C2307FBB700F5032A /* FLEXKeyChainTableViewController.h */,
+				71E1C2102307FBB700F5032A /* FLEXKeyChainTableViewController.m */,
+			);
+			path = KeyChain;
+			sourceTree = "<group>";
+		};
 		779B1EBF1C0C4D7C001F5E49 /* DatabaseBrowser */ = {
 			isa = PBXGroup;
 			children = (
@@ -896,6 +922,7 @@
 				3A4C95031B5B21410088C3F2 /* FLEXArgumentInputView.h in Headers */,
 				94A5151D1C4CA1F10063292F /* FLEXExplorerViewController.h in Headers */,
 				C3F31D3F2267D883003C991A /* FLEXMultilineTableViewCell.h in Headers */,
+				71E1C2132307FBB800F5032A /* FLEXKeychain.h in Headers */,
 				3A4C94C51B5B21410088C3F2 /* FLEXArrayExplorerViewController.h in Headers */,
 				C3F31D432267D883003C991A /* FLEXTableView.h in Headers */,
 				3A4C94CB1B5B21410088C3F2 /* FLEXDictionaryExplorerViewController.h in Headers */,
@@ -909,6 +936,7 @@
 				779B1ED41C0C4D7C001F5E49 /* FLEXTableContentCell.h in Headers */,
 				3A4C952C1B5B21410088C3F2 /* FLEXLiveObjectsTableViewController.h in Headers */,
 				3A4C94EF1B5B21410088C3F2 /* FLEXArgumentInputDateView.h in Headers */,
+				71E1C2142307FBB800F5032A /* FLEXKeyChainTableViewController.h in Headers */,
 				3A4C94C71B5B21410088C3F2 /* FLEXClassExplorerViewController.h in Headers */,
 				3A4C94F71B5B21410088C3F2 /* FLEXArgumentInputNotSupportedView.h in Headers */,
 				C3F31D3E2267D883003C991A /* FLEXTableViewCell.h in Headers */,
@@ -939,6 +967,7 @@
 				3A4C95401B5B21410088C3F2 /* FLEXNetworkTransactionTableViewCell.h in Headers */,
 				3A4C95241B5B21410088C3F2 /* FLEXFileBrowserTableViewController.h in Headers */,
 				94AAF0381BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.h in Headers */,
+				71E1C2152307FBB800F5032A /* FLEXKeychainQuery.h in Headers */,
 				94AAF03A1BAF2F0300DE8760 /* FLEXKeyboardShortcutManager.h in Headers */,
 				C395D6D921789BD800BEAD4D /* FLEXColorExplorerViewController.h in Headers */,
 				94A515181C4CA1D70063292F /* FLEXManager+Private.h in Headers */,
@@ -1087,6 +1116,7 @@
 				3A4C94D01B5B21410088C3F2 /* FLEXImageExplorerViewController.m in Sources */,
 				679F64871BD53B7B00A8C94C /* FLEXCookiesTableViewController.m in Sources */,
 				3A4C94CE1B5B21410088C3F2 /* FLEXGlobalsEntry.m in Sources */,
+				71E1C2192307FBB800F5032A /* FLEXKeychainQuery.m in Sources */,
 				3A4C94FE1B5B21410088C3F2 /* FLEXArgumentInputStructView.m in Sources */,
 				C3F31D402267D883003C991A /* FLEXSubtitleTableViewCell.m in Sources */,
 				3A4C95431B5B21410088C3F2 /* FLEXNetworkObserver.m in Sources */,
@@ -1115,6 +1145,7 @@
 				3A4C94F81B5B21410088C3F2 /* FLEXArgumentInputNotSupportedView.m in Sources */,
 				3A4C95351B5B21410088C3F2 /* FLEXSystemLogTableViewController.m in Sources */,
 				3A4C95271B5B21410088C3F2 /* FLEXGlobalsTableViewController.m in Sources */,
+				71E1C2172307FBB800F5032A /* FLEXKeychain.m in Sources */,
 				C387C88422E0D24A00750E58 /* UIView+Layout.m in Sources */,
 				779B1ED31C0C4D7C001F5E49 /* FLEXTableColumnHeader.m in Sources */,
 				C37A0C94218BAC9600848CA7 /* FLEXObjcInternal.mm in Sources */,
@@ -1145,6 +1176,7 @@
 				3A4C95061B5B21410088C3F2 /* FLEXArgumentInputViewFactory.m in Sources */,
 				3A4C95291B5B21410088C3F2 /* FLEXInstancesTableViewController.m in Sources */,
 				3A4C952B1B5B21410088C3F2 /* FLEXLibrariesTableViewController.m in Sources */,
+				71E1C2182307FBB800F5032A /* FLEXKeyChainTableViewController.m in Sources */,
 				94A5151E1C4CA1F10063292F /* FLEXExplorerViewController.m in Sources */,
 				3A4C95371B5B21410088C3F2 /* FLEXNetworkHistoryTableViewController.m in Sources */,
 				C3511B9222D7C99E0057BAB7 /* FLEXTableViewSection.m in Sources */,