mirror of
				https://github.com/SoPat712/YTLitePlus.git
				synced 2025-10-30 20:34:04 -04:00 
			
		
		
		
	added files via upload
This commit is contained in:
		
							
								
								
									
										144
									
								
								Tweaks/FLEX/GlobalStateExplorers/Keychain/FLEXKeychain.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								Tweaks/FLEX/GlobalStateExplorers/Keychain/FLEXKeychain.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| // | ||||
| //  FLEXKeychain.h | ||||
| // | ||||
| //  Derived from: | ||||
| //  SSKeychain.h in SSKeychain | ||||
| //  Created by Sam Soffes on 5/19/10. | ||||
| //  Copyright (c) 2010-2014 Sam Soffes. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
|  | ||||
| /// Error code specific to FLEXKeychain 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, FLEXKeychainErrorCode) { | ||||
|     /// Some of the arguments were invalid. | ||||
|     FLEXKeychainErrorBadArguments = -1001, | ||||
| }; | ||||
|  | ||||
| /// FLEXKeychain error domain | ||||
| extern NSString *const kFLEXKeychainErrorDomain; | ||||
|  | ||||
| /// Account name. | ||||
| extern NSString *const kFLEXKeychainAccountKey; | ||||
|  | ||||
| /// Time the item was created. | ||||
| /// | ||||
| /// The value will be a string. | ||||
| extern NSString *const kFLEXKeychainCreatedAtKey; | ||||
|  | ||||
| /// Item class. | ||||
| extern NSString *const kFLEXKeychainClassKey; | ||||
|  | ||||
| /// Item description. | ||||
| extern NSString *const kFLEXKeychainDescriptionKey; | ||||
|  | ||||
| /// Item group. | ||||
| extern NSString *const kFLEXKeychainGroupKey; | ||||
|  | ||||
| /// Item label. | ||||
| extern NSString *const kFLEXKeychainLabelKey; | ||||
|  | ||||
| /// Time the item was last modified. | ||||
| /// | ||||
| /// The value will be a string. | ||||
| extern NSString *const kFLEXKeychainLastModifiedKey; | ||||
|  | ||||
| /// Where the item was created. | ||||
| extern NSString *const kFLEXKeychainWhereKey; | ||||
|  | ||||
| /// A simple wrapper for accessing accounts, getting passwords, | ||||
| /// setting passwords, and deleting passwords using the system Keychain. | ||||
| @interface FLEXKeychain : NSObject | ||||
|  | ||||
| #pragma mark - Classic methods | ||||
|  | ||||
| /// @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; | ||||
|  | ||||
| /// 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; | ||||
|  | ||||
|  | ||||
| /// 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; | ||||
|  | ||||
|  | ||||
| /// 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; | ||||
|  | ||||
| /// 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; | ||||
|  | ||||
| /// @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. | ||||
| /// | ||||
| /// @note See the `NSString` constants declared in FLEXKeychain.h for a list of keys that | ||||
| /// can be used when accessing the dictionaries returned by this method. | ||||
| + (NSArray<NSDictionary<NSString *, id> *> *)allAccounts; | ||||
| + (NSArray<NSDictionary<NSString *, id> *> *)allAccounts:(NSError *__autoreleasing *)error; | ||||
|  | ||||
| /// @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. | ||||
| /// | ||||
| /// @note See the `NSString` constants declared in FLEXKeychain.h for a list of keys that | ||||
| /// can be used when accessing the dictionaries returned by this method. | ||||
| + (NSArray<NSDictionary<NSString *, id> *> *)accountsForService:(NSString *)serviceName; | ||||
| + (NSArray<NSDictionary<NSString *, id> *> *)accountsForService:(NSString *)serviceName error:(NSError *__autoreleasing *)error; | ||||
|  | ||||
|  | ||||
| #pragma mark - Configuration | ||||
|  | ||||
| #if __IPHONE_4_0 && TARGET_OS_IPHONE | ||||
| /// Returns the accessibility type for all future passwords saved to the Keychain. | ||||
| /// | ||||
| /// @return `NULL` or one of the "Keychain Item Accessibility | ||||
| /// Constants" used for determining when a keychain item should be readable. | ||||
| + (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. | ||||
| /// | ||||
| /// @note See Security/SecItem.h | ||||
| + (void)setAccessibilityType:(CFTypeRef)accessibilityType; | ||||
| #endif | ||||
|  | ||||
| @end | ||||
|  | ||||
							
								
								
									
										121
									
								
								Tweaks/FLEX/GlobalStateExplorers/Keychain/FLEXKeychain.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								Tweaks/FLEX/GlobalStateExplorers/Keychain/FLEXKeychain.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| // | ||||
| //  FLEXKeychain.m | ||||
| // | ||||
| //  Forked from: | ||||
| //  SSKeychain.m in 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 kFLEXKeychainErrorDomain = @"com.flipboard.flex"; | ||||
| NSString * const kFLEXKeychainAccountKey = @"acct"; | ||||
| NSString * const kFLEXKeychainCreatedAtKey = @"cdat"; | ||||
| NSString * const kFLEXKeychainClassKey = @"labl"; | ||||
| NSString * const kFLEXKeychainDescriptionKey = @"desc"; | ||||
| NSString * const kFLEXKeychainGroupKey = @"agrp"; | ||||
| NSString * const kFLEXKeychainLabelKey = @"labl"; | ||||
| NSString * const kFLEXKeychainLastModifiedKey = @"mdat"; | ||||
| NSString * const kFLEXKeychainWhereKey = @"svce"; | ||||
|  | ||||
| #if __IPHONE_4_0 && TARGET_OS_IPHONE | ||||
| static CFTypeRef FLEXKeychainAccessibilityType = 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 new]; | ||||
|     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 new]; | ||||
|     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 new]; | ||||
|     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 new]; | ||||
|     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 new]; | ||||
|     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 new]; | ||||
|     query.service = serviceName; | ||||
|     return [query fetchAll:error]; | ||||
| } | ||||
|  | ||||
| #if __IPHONE_4_0 && TARGET_OS_IPHONE | ||||
| + (CFTypeRef)accessibilityType { | ||||
|     return FLEXKeychainAccessibilityType; | ||||
| } | ||||
|  | ||||
| + (void)setAccessibilityType:(CFTypeRef)accessibilityType { | ||||
|     CFRetain(accessibilityType); | ||||
|     if (FLEXKeychainAccessibilityType) { | ||||
|         CFRelease(FLEXKeychainAccessibilityType); | ||||
|     } | ||||
|     FLEXKeychainAccessibilityType = accessibilityType; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| @end | ||||
							
								
								
									
										112
									
								
								Tweaks/FLEX/GlobalStateExplorers/Keychain/FLEXKeychainQuery.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								Tweaks/FLEX/GlobalStateExplorers/Keychain/FLEXKeychainQuery.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| // | ||||
| //  FLEXKeychainQuery.h | ||||
| // | ||||
| //  Derived from: | ||||
| //  SSKeychainQuery.h in SSKeychain | ||||
| //  Created by Caleb Davenport on 3/19/13. | ||||
| //  Copyright (c) 2010-2014 Sam Soffes. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
| #import <Security/Security.h> | ||||
|  | ||||
| #if __IPHONE_7_0 || __MAC_10_9 | ||||
| // Keychain synchronization available at compile time | ||||
| #define FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE 1 | ||||
| #endif | ||||
|  | ||||
| #if __IPHONE_3_0 || __MAC_10_9 | ||||
| // Keychain access group available at compile time | ||||
| #define FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE 1 | ||||
| #endif | ||||
|  | ||||
| #ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE | ||||
| typedef NS_ENUM(NSUInteger, FLEXKeychainQuerySynchronizationMode) { | ||||
|     FLEXKeychainQuerySynchronizationModeAny, | ||||
|     FLEXKeychainQuerySynchronizationModeNo, | ||||
|     FLEXKeychainQuerySynchronizationModeYes | ||||
| }; | ||||
| #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 FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE | ||||
| /// kSecAttrAccessGroup (only used on iOS) | ||||
| @property (nonatomic, copy) NSString *accessGroup; | ||||
| #endif | ||||
|  | ||||
| #ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE | ||||
| /// kSecAttrSynchronizable | ||||
| @property (nonatomic) FLEXKeychainQuerySynchronizationMode 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; | ||||
|  | ||||
|  | ||||
| #pragma mark 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; | ||||
|  | ||||
|  | ||||
| #pragma mark 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; | ||||
|  | ||||
|  | ||||
| #pragma mark Synchronization Status | ||||
|  | ||||
| #ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE | ||||
| /// Returns a boolean indicating if keychain synchronization is available on the device at runtime. | ||||
| /// The #define FLEXKEYCHAIN_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 | ||||
							
								
								
									
										304
									
								
								Tweaks/FLEX/GlobalStateExplorers/Keychain/FLEXKeychainQuery.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										304
									
								
								Tweaks/FLEX/GlobalStateExplorers/Keychain/FLEXKeychainQuery.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,304 @@ | ||||
| // | ||||
| //  FLEXKeychainQuery.m | ||||
| //  FLEXKeychain | ||||
| // | ||||
| //  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 | ||||
|  | ||||
| #pragma mark - Public | ||||
|  | ||||
| - (BOOL)save:(NSError *__autoreleasing *)error { | ||||
|     OSStatus status = FLEXKeychainErrorBadArguments; | ||||
|     if (!self.service || !self.account || !self.passwordData) { | ||||
|         if (error) { | ||||
|             *error = [self 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[(__bridge id)kSecValueData] = self.passwordData; | ||||
| #if __IPHONE_4_0 && TARGET_OS_IPHONE | ||||
|         CFTypeRef accessibilityType = FLEXKeychain.accessibilityType; | ||||
|         if (accessibilityType) { | ||||
|             query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType; | ||||
|         } | ||||
| #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[(__bridge id)kSecAttrLabel] = self.label; | ||||
|         } | ||||
|         query[(__bridge id)kSecValueData] = self.passwordData; | ||||
| #if __IPHONE_4_0 && TARGET_OS_IPHONE | ||||
|         CFTypeRef accessibilityType = FLEXKeychain.accessibilityType; | ||||
|         if (accessibilityType) { | ||||
|             query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType; | ||||
|         } | ||||
| #endif | ||||
|         status = SecItemAdd((__bridge CFDictionaryRef)query, NULL); | ||||
|     } | ||||
|      | ||||
|     if (status != errSecSuccess && error != NULL) { | ||||
|         *error = [self errorWithCode:status]; | ||||
|     } | ||||
|      | ||||
|     return (status == errSecSuccess); | ||||
| } | ||||
|  | ||||
|  | ||||
| - (BOOL)deleteItem:(NSError *__autoreleasing *)error { | ||||
|     OSStatus status = FLEXKeychainErrorBadArguments; | ||||
|     if (!self.service || !self.account) { | ||||
|         if (error) { | ||||
|             *error = [self 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[(__bridge id)kSecReturnRef] = @YES; | ||||
|     status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); | ||||
|     if (status == errSecSuccess) { | ||||
|         status = SecKeychainItemDelete((SecKeychainItemRef)result); | ||||
|         CFRelease(result); | ||||
|     } | ||||
| #endif | ||||
|      | ||||
|     if (status != errSecSuccess && error != NULL) { | ||||
|         *error = [self errorWithCode:status]; | ||||
|     } | ||||
|      | ||||
|     return (status == errSecSuccess); | ||||
| } | ||||
|  | ||||
|  | ||||
| - (NSArray *)fetchAll:(NSError *__autoreleasing *)error { | ||||
|     NSMutableDictionary *query = [self query]; | ||||
|     query[(__bridge id)kSecReturnAttributes] = @YES; | ||||
|     query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll; | ||||
| #if __IPHONE_4_0 && TARGET_OS_IPHONE | ||||
|     CFTypeRef accessibilityType = FLEXKeychain.accessibilityType; | ||||
|     if (accessibilityType) { | ||||
|         query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType; | ||||
|     } | ||||
| #endif | ||||
|      | ||||
|     CFTypeRef result = NULL; | ||||
|     OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); | ||||
|     if (status != errSecSuccess && error != NULL) { | ||||
|         *error = [self errorWithCode:status]; | ||||
|         return nil; | ||||
|     } | ||||
|      | ||||
|     return (__bridge_transfer NSArray *)result ?: @[]; | ||||
| } | ||||
|  | ||||
|  | ||||
| - (BOOL)fetch:(NSError *__autoreleasing *)error { | ||||
|     OSStatus status = FLEXKeychainErrorBadArguments; | ||||
|     if (!self.service || !self.account) { | ||||
|         if (error) { | ||||
|             *error = [self errorWithCode:status]; | ||||
|         } | ||||
|         return NO; | ||||
|     } | ||||
|      | ||||
|     CFTypeRef result = NULL; | ||||
|     NSMutableDictionary *query = [self query]; | ||||
|     query[(__bridge id)kSecReturnData] = @YES; | ||||
|     query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne; | ||||
|     status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); | ||||
|      | ||||
|     if (status != errSecSuccess) { | ||||
|         if (error) { | ||||
|             *error = [self 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 FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE | ||||
| + (BOOL)isSynchronizationAvailable { | ||||
| #if TARGET_OS_IPHONE | ||||
|     return YES; | ||||
| #else | ||||
|     return floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_8_4; | ||||
| #endif | ||||
| } | ||||
| #endif | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| - (NSMutableDictionary *)query { | ||||
|     NSMutableDictionary *dictionary = [NSMutableDictionary new]; | ||||
|     dictionary[(__bridge id)kSecClass] = (__bridge id)kSecClassGenericPassword; | ||||
|      | ||||
|     if (self.service) { | ||||
|         dictionary[(__bridge id)kSecAttrService] = self.service; | ||||
|     } | ||||
|      | ||||
|     if (self.account) { | ||||
|         dictionary[(__bridge id)kSecAttrAccount] = self.account; | ||||
|     } | ||||
|      | ||||
| #ifdef FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE | ||||
| #if !TARGET_IPHONE_SIMULATOR | ||||
|     if (self.accessGroup) { | ||||
|         dictionary[(__bridge id)kSecAttrAccessGroup] = self.accessGroup; | ||||
|     } | ||||
| #endif | ||||
| #endif | ||||
|      | ||||
| #ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE | ||||
|     if ([[self class] isSynchronizationAvailable]) { | ||||
|         id value; | ||||
|          | ||||
|         switch (self.synchronizationMode) { | ||||
|             case FLEXKeychainQuerySynchronizationModeNo: { | ||||
|                 value = @NO; | ||||
|                 break; | ||||
|             } | ||||
|             case FLEXKeychainQuerySynchronizationModeYes: { | ||||
|                 value = @YES; | ||||
|                 break; | ||||
|             } | ||||
|             case FLEXKeychainQuerySynchronizationModeAny: { | ||||
|                 value = (__bridge id)(kSecAttrSynchronizableAny); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         dictionary[(__bridge id)(kSecAttrSynchronizable)] = value; | ||||
|     } | ||||
| #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:@"FLEXKeychain" withExtension:@"bundle"]; | ||||
|         resourcesBundle = [NSBundle bundleWithURL:url]; | ||||
|     }); | ||||
|      | ||||
|     NSString *message = nil; | ||||
|     switch (code) { | ||||
|         case errSecSuccess: return nil; | ||||
|         case FLEXKeychainErrorBadArguments: message = NSLocalizedStringFromTableInBundle(@"FLEXKeychainErrorBadArguments", @"FLEXKeychain", resourcesBundle, nil); break; | ||||
|              | ||||
| #if TARGET_OS_IPHONE | ||||
|         case errSecUnimplemented: { | ||||
|             message = NSLocalizedStringFromTableInBundle(@"errSecUnimplemented", @"FLEXKeychain", resourcesBundle, nil); | ||||
|             break; | ||||
|         } | ||||
|         case errSecParam: { | ||||
|             message = NSLocalizedStringFromTableInBundle(@"errSecParam", @"FLEXKeychain", resourcesBundle, nil); | ||||
|             break; | ||||
|         } | ||||
|         case errSecAllocate: { | ||||
|             message = NSLocalizedStringFromTableInBundle(@"errSecAllocate", @"FLEXKeychain", resourcesBundle, nil); | ||||
|             break; | ||||
|         } | ||||
|         case errSecNotAvailable: { | ||||
|             message = NSLocalizedStringFromTableInBundle(@"errSecNotAvailable", @"FLEXKeychain", resourcesBundle, nil); | ||||
|             break; | ||||
|         } | ||||
|         case errSecDuplicateItem: { | ||||
|             message = NSLocalizedStringFromTableInBundle(@"errSecDuplicateItem", @"FLEXKeychain", resourcesBundle, nil); | ||||
|             break; | ||||
|         } | ||||
|         case errSecItemNotFound: { | ||||
|             message = NSLocalizedStringFromTableInBundle(@"errSecItemNotFound", @"FLEXKeychain", resourcesBundle, nil); | ||||
|             break; | ||||
|         } | ||||
|         case errSecInteractionNotAllowed: { | ||||
|             message = NSLocalizedStringFromTableInBundle(@"errSecInteractionNotAllowed", @"FLEXKeychain", resourcesBundle, nil); | ||||
|             break; | ||||
|         } | ||||
|         case errSecDecode: { | ||||
|             message = NSLocalizedStringFromTableInBundle(@"errSecDecode", @"FLEXKeychain", resourcesBundle, nil); | ||||
|             break; | ||||
|         } | ||||
|         case errSecAuthFailed: { | ||||
|             message = NSLocalizedStringFromTableInBundle(@"errSecAuthFailed", @"FLEXKeychain", resourcesBundle, nil); | ||||
|             break; | ||||
|         } | ||||
|         default: { | ||||
|             message = NSLocalizedStringFromTableInBundle(@"errSecDefault", @"FLEXKeychain", resourcesBundle, nil); | ||||
|         } | ||||
| #else | ||||
|         default: | ||||
|             message = (__bridge_transfer NSString *)SecCopyErrorMessageString(code, NULL); | ||||
| #endif | ||||
|     } | ||||
|      | ||||
|     NSDictionary *userInfo = message ? @{ NSLocalizedDescriptionKey : message } : nil; | ||||
|     return [NSError errorWithDomain:kFLEXKeychainErrorDomain code:code userInfo:userInfo]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,14 @@ | ||||
| // | ||||
| //  FLEXKeychainViewController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by ray on 2019/8/17. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXGlobalsEntry.h" | ||||
| #import "FLEXFilteringTableViewController.h" | ||||
|  | ||||
| @interface FLEXKeychainViewController : FLEXFilteringTableViewController <FLEXGlobalsEntry> | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,254 @@ | ||||
| // | ||||
| //  FLEXKeychainViewController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by ray on 2019/8/17. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXKeychain.h" | ||||
| #import "FLEXKeychainQuery.h" | ||||
| #import "FLEXKeychainViewController.h" | ||||
| #import "FLEXTableViewCell.h" | ||||
| #import "FLEXMutableListSection.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "UIPasteboard+FLEX.h" | ||||
| #import "UIBarButtonItem+FLEX.h" | ||||
|  | ||||
| @interface FLEXKeychainViewController () | ||||
| @property (nonatomic, readonly) FLEXMutableListSection<NSDictionary *> *section; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXKeychainViewController | ||||
|  | ||||
| - (id)init { | ||||
|     return [self initWithStyle:UITableViewStyleGrouped]; | ||||
| } | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|      | ||||
|     [self addToolbarItems:@[ | ||||
|         FLEXBarButtonItemSystem(Add, self, @selector(addPressed)), | ||||
|         [FLEXBarButtonItemSystem(Trash, self, @selector(trashPressed:)) flex_withTintColor:UIColor.redColor], | ||||
|     ]]; | ||||
|  | ||||
|     [self reloadData]; | ||||
| } | ||||
|  | ||||
| - (NSArray<FLEXTableViewSection *> *)makeSections { | ||||
|     _section = [FLEXMutableListSection list:FLEXKeychain.allAccounts.mutableCopy | ||||
|         cellConfiguration:^(__kindof FLEXTableViewCell *cell, NSDictionary *item, NSInteger row) { | ||||
|             cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; | ||||
|          | ||||
|             id service = item[kFLEXKeychainWhereKey]; | ||||
|             if ([service isKindOfClass:[NSString class]]) { | ||||
|                 cell.textLabel.text = service; | ||||
|                 cell.detailTextLabel.text = [item[kFLEXKeychainAccountKey] description]; | ||||
|             } else { | ||||
|                 cell.textLabel.text = [NSString stringWithFormat: | ||||
|                     @"[%@]\n\n%@", | ||||
|                     NSStringFromClass([service class]), | ||||
|                     [service description] | ||||
|                 ]; | ||||
|             } | ||||
|         } filterMatcher:^BOOL(NSString *filterText, NSDictionary *item) { | ||||
|             // Loop over contents of the keychain item looking for a match | ||||
|             for (NSString *field in item.allValues) { | ||||
|                 if ([field isKindOfClass:[NSString class]]) { | ||||
|                     if ([field localizedCaseInsensitiveContainsString:filterText]) { | ||||
|                         return YES; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             return NO; | ||||
|         } | ||||
|     ]; | ||||
|      | ||||
|     return @[self.section]; | ||||
| } | ||||
|  | ||||
| /// We always want to show this section | ||||
| - (NSArray<FLEXTableViewSection *> *)nonemptySections { | ||||
|     return @[self.section]; | ||||
| } | ||||
|  | ||||
| - (void)reloadSections { | ||||
|     self.section.list = FLEXKeychain.allAccounts.mutableCopy; | ||||
| } | ||||
|  | ||||
| - (void)refreshSectionTitle { | ||||
|     self.section.customTitle = FLEXPluralString( | ||||
|         self.section.filteredList.count, @"items", @"item" | ||||
|     ); | ||||
| } | ||||
|  | ||||
| - (void)reloadData { | ||||
|     [self reloadSections]; | ||||
|     [self refreshSectionTitle]; | ||||
|     [super reloadData]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| - (FLEXKeychainQuery *)queryForItemAtIndex:(NSInteger)idx { | ||||
|     NSDictionary *item = self.section.filteredList[idx]; | ||||
|  | ||||
|     FLEXKeychainQuery *query = [FLEXKeychainQuery new]; | ||||
|     query.service = [item[kFLEXKeychainWhereKey] description]; | ||||
|     query.account = [item[kFLEXKeychainAccountKey] description]; | ||||
|     query.accessGroup = [item[kFLEXKeychainGroupKey] description]; | ||||
|     [query fetch:nil]; | ||||
|  | ||||
|     return query; | ||||
| } | ||||
|  | ||||
| - (void)deleteItem:(NSDictionary *)item { | ||||
|     NSError *error = nil; | ||||
|     BOOL success = [FLEXKeychain | ||||
|         deletePasswordForService:item[kFLEXKeychainWhereKey] | ||||
|         account:item[kFLEXKeychainAccountKey] | ||||
|         error:&error | ||||
|     ]; | ||||
|  | ||||
|     if (!success) { | ||||
|         [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|             make.title(@"Error Deleting Item"); | ||||
|             make.message(error.localizedDescription); | ||||
|         } showFrom:self]; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark Buttons | ||||
|  | ||||
| - (void)trashPressed:(UIBarButtonItem *)sender { | ||||
|     [FLEXAlert makeSheet:^(FLEXAlert *make) { | ||||
|         make.title(@"Clear Keychain"); | ||||
|         make.message(@"This will remove all keychain items for this app.\n"); | ||||
|         make.message(@"This action cannot be undone. Are you sure?"); | ||||
|         make.button(@"Yes, clear the keychain").destructiveStyle().handler(^(NSArray *strings) { | ||||
|             [self confirmClearKeychain]; | ||||
|         }); | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:self source:sender]; | ||||
| } | ||||
|  | ||||
| - (void)confirmClearKeychain { | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(@"ARE YOU SURE?"); | ||||
|         make.message(@"This action CANNOT BE UNDONE.\nAre you sure you want to continue?\n"); | ||||
|         make.message(@"If you're sure, scroll to confirm."); | ||||
|         make.button(@"Yes, clear the keychain").destructiveStyle().handler(^(NSArray *strings) { | ||||
|             for (id account in self.section.list) { | ||||
|                 [self deleteItem:account]; | ||||
|             } | ||||
|  | ||||
|             [self reloadData]; | ||||
|         }); | ||||
|         make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); | ||||
|         make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); | ||||
|         make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); | ||||
|         make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
| - (void)addPressed { | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(@"Add Keychain Item"); | ||||
|         make.textField(@"Service name, i.e. Instagram"); | ||||
|         make.textField(@"Account"); | ||||
|         make.textField(@"Password"); | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|         make.button(@"Save").handler(^(NSArray<NSString *> *strings) { | ||||
|             // Display errors | ||||
|             NSError *error = nil; | ||||
|             if (![FLEXKeychain setPassword:strings[2] forService:strings[0] account:strings[1] error:&error]) { | ||||
|                 [FLEXAlert showAlert:@"Error" message:error.localizedDescription from:self]; | ||||
|             } | ||||
|  | ||||
|             [self reloadData]; | ||||
|         }); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - FLEXGlobalsEntry | ||||
|  | ||||
| + (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row { | ||||
|     return @"🔑  Keychain"; | ||||
| } | ||||
|  | ||||
| + (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row { | ||||
|     FLEXKeychainViewController *viewController = [self new]; | ||||
|     viewController.title = [self globalsEntryTitle:row]; | ||||
|  | ||||
|     return viewController; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Table View Data Source | ||||
|  | ||||
| - (void)tableView:(UITableView *)tv commitEditingStyle:(UITableViewCellEditingStyle)style forRowAtIndexPath:(NSIndexPath *)ip { | ||||
|     if (style == UITableViewCellEditingStyleDelete) { | ||||
|         // Update the model | ||||
|         NSDictionary *toRemove = self.section.filteredList[ip.row]; | ||||
|         [self deleteItem:toRemove]; | ||||
|         [self.section mutate:^(NSMutableArray *list) { | ||||
|             [list removeObject:toRemove]; | ||||
|         }]; | ||||
|      | ||||
|         // Delete the row | ||||
|         [tv deleteRowsAtIndexPaths:@[ip] withRowAnimation:UITableViewRowAnimationAutomatic]; | ||||
|          | ||||
|         // Update the title by refreshing the section without disturbing the delete animation | ||||
|         // | ||||
|         // This is an ugly hack, but literally nothing else works, save for manually getting | ||||
|         // the header and setting its title, which I personally think is worse since it | ||||
|         // would need to make assumptions about the default style of the header (CAPS) | ||||
|         dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ | ||||
|             [self refreshSectionTitle]; | ||||
|             [tv reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationNone]; | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Table View Delegate | ||||
|  | ||||
| - (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     FLEXKeychainQuery *query = [self queryForItemAtIndex:indexPath.row]; | ||||
|      | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(query.service); | ||||
|         make.message(@"Service: ").message(query.service); | ||||
|         make.message(@"\nAccount: ").message(query.account); | ||||
|         make.message(@"\nPassword: ").message(query.password); | ||||
|         make.message(@"\nGroup: ").message(query.accessGroup); | ||||
|  | ||||
|         make.button(@"Copy Service").handler(^(NSArray<NSString *> *strings) { | ||||
|             [UIPasteboard.generalPasteboard flex_copy:query.service]; | ||||
|         }); | ||||
|         make.button(@"Copy Account").handler(^(NSArray<NSString *> *strings) { | ||||
|             [UIPasteboard.generalPasteboard flex_copy:query.account]; | ||||
|         }); | ||||
|         make.button(@"Copy Password").handler(^(NSArray<NSString *> *strings) { | ||||
|             [UIPasteboard.generalPasteboard flex_copy:query.password]; | ||||
|         }); | ||||
|         make.button(@"Dismiss").cancelStyle(); | ||||
|          | ||||
|     } showFrom:self]; | ||||
|  | ||||
|     [tableView deselectRowAtIndexPath:indexPath animated:YES]; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										20
									
								
								Tweaks/FLEX/GlobalStateExplorers/Keychain/SSKeychain_LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								Tweaks/FLEX/GlobalStateExplorers/Keychain/SSKeychain_LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| Copyright (c) 2010-2012 Sam Soffes. | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining | ||||
| a copy of this software and associated documentation files (the | ||||
| "Software"), to deal in the Software without restriction, including | ||||
| without limitation the rights to use, copy, modify, merge, publish, | ||||
| distribute, sublicense, and/or sell copies of the Software, and to | ||||
| permit persons to whom the Software is furnished to do so, subject to | ||||
| the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be | ||||
| included in all copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||||
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||||
| NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||||
| LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||||
| OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||||
| WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||||
		Reference in New Issue
	
	Block a user
	 Balackburn
					Balackburn