Add test app context; use mock "document" and "shared data container" directories in tests, use mock keychain storage in tests.

This commit is contained in:
Matthew Chen 2018-08-31 13:01:47 -04:00
parent 0357699fc8
commit 399dd13cee
12 changed files with 468 additions and 15 deletions

View File

@ -11,10 +11,10 @@
#import "TSAccountManager.h"
#import "UIView+OWS.h"
#import "ViewControllerUtils.h"
#import <SAMKeychain/SAMKeychain.h>
#import <SignalMessaging/Environment.h>
#import <SignalMessaging/NSString+OWS.h>
#import <SignalMessaging/OWSNavigationController.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
@ -566,11 +566,12 @@ NSString *const kKeychainKey_LastRegisteredPhoneNumber = @"kKeychainKey_LastRegi
OWSCAssertDebug(key.length > 0);
NSError *error;
NSString *value = [SAMKeychain passwordForService:kKeychainService_LastRegistered account:key error:&error];
if (value && !error) {
return value;
NSString *_Nullable value =
[CurrentAppContext().keychainStorage stringForKey:key service:kKeychainService_LastRegistered error:&error];
if (error || !value) {
DDLogWarn(@"Could not retrieve 'last registered' value from keychain: %@.", error);
}
return nil;
return value;
}
- (void)setDebugValue:(NSString *)value forKey:(NSString *)key
@ -580,8 +581,10 @@ NSString *const kKeychainKey_LastRegisteredPhoneNumber = @"kKeychainKey_LastRegi
OWSCAssertDebug(value.length > 0);
NSError *error;
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly];
BOOL success = [SAMKeychain setPassword:value forService:kKeychainService_LastRegistered account:key error:&error];
BOOL success = [CurrentAppContext().keychainStorage setWithString:value
forKey:key
service:kKeychainService_LastRegistered
error:&error];
if (!success || error) {
OWSLogError(@"Error persisting 'last registered' value in keychain: %@", error);
}

View File

@ -283,6 +283,26 @@ NS_ASSUME_NONNULL_BEGIN
backgroundTask = nil;
}
- (id<KeychainStorage>)keychainStorage
{
return [SSKKeychainStorage sharedInstance];
}
- (NSString *)appDocumentDirectoryPath
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentDirectoryURL =
[[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
return [documentDirectoryURL path];
}
- (NSString *)appSharedDataDirectoryPath
{
NSURL *groupContainerDirectoryURL =
[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:SignalApplicationGroup];
return [groupContainerDirectoryURL path];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -6,7 +6,6 @@
#import "OWSBackupIO.h"
#import "Signal-Swift.h"
#import <Curve25519Kit/Randomness.h>
#import <SAMKeychain/SAMKeychain.h>
#import <YapDatabase/YapDatabaseCryptoUtils.h>
NS_ASSUME_NONNULL_BEGIN

View File

@ -0,0 +1,103 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import SAMKeychain
public enum KeychainStorageError: Error {
case failure(description: String)
}
// MARK: -
@objc public protocol KeychainStorage: class {
@objc func string(forKey key: String, service: String) throws -> String
@objc func set(string: String, forKey key: String, service: String) throws
@objc func data(forKey key: String, service: String) throws -> Data
@objc func set(data: Data, forKey key: String, service: String) throws
@objc func remove(key: String, service: String) throws
}
// MARK: -
@objc
public class SSKKeychainStorage: NSObject, KeychainStorage {
@objc public static let sharedInstance = SSKKeychainStorage()
// Force usage as a singleton
override private init() {
super.init()
SwiftSingletons.register(self)
}
@objc public func string(forKey key: String, service: String) throws -> String {
var error: NSError?
let result = SAMKeychain.password(forService: service, account: key, error: &error)
if let error = error {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) error retrieving string: \(error)")
}
guard let string = result else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) could not retrieve string")
}
return string
}
@objc public func set(string: String, forKey key: String, service: String) throws {
SAMKeychain.setAccessibilityType(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)
var error: NSError?
let result = SAMKeychain.setPassword(string, forService: service, account: key, error: &error)
if let error = error {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) error setting string: \(error)")
}
guard result else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) could not set string")
}
}
@objc public func data(forKey key: String, service: String) throws -> Data {
var error: NSError?
let result = SAMKeychain.passwordData(forService: service, account: key, error: &error)
if let error = error {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) error retrieving data: \(error)")
}
guard let data = result else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) could not retrieve data")
}
return data
}
@objc public func set(data: Data, forKey key: String, service: String) throws {
SAMKeychain.setAccessibilityType(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)
var error: NSError?
let result = SAMKeychain.setPasswordData(data, forService: service, account: key, error: &error)
if let error = error {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) error setting data: \(error)")
}
guard result else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) could not set data")
}
}
@objc public func remove(key: String, service: String) throws {
var error: NSError?
let result = SAMKeychain.deletePassword(forService: service, account: key, error: &error)
if let error = error {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) error removing data: \(error)")
}
guard result else {
throw SSKProtoError.invalidProtobuf(description: "\(logTag) could not remove data")
}
}
}

View File

@ -0,0 +1,13 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "AppContext.h"
NS_ASSUME_NONNULL_BEGIN
@interface TestAppContext : NSObject <AppContext>
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,153 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "TestAppContext.h"
#import "TestKeychainStorage.h"
NS_ASSUME_NONNULL_BEGIN
@interface TestAppContext ()
@property (nonatomic) TestKeychainStorage *testKeychainStorage;
@property (nonatomic) NSString *appDocumentDirPath;
@property (nonatomic) NSString *appSharedDataDirPath;
@end
#pragma mark -
@implementation TestAppContext
@synthesize mainWindow = _mainWindow;
@synthesize appLaunchTime = _appLaunchTime;
- (instancetype)init
{
self = [super init];
if (!self) {
return self;
}
self.testKeychainStorage = [TestKeychainStorage new];
NSString *temporaryDirectory = NSTemporaryDirectory();
self.appDocumentDirPath = [temporaryDirectory stringByAppendingPathComponent:NSUUID.UUID.UUIDString];
self.appSharedDataDirPath = [temporaryDirectory stringByAppendingPathComponent:NSUUID.UUID.UUIDString];
_appLaunchTime = [NSDate new];
return self;
}
- (UIApplicationState)reportedApplicationState
{
return UIApplicationStateActive;
}
#pragma mark -
- (BOOL)isMainApp
{
return YES;
}
- (BOOL)isMainAppAndActive
{
return YES;
}
- (BOOL)isRTL
{
return NO;
}
- (void)setStatusBarHidden:(BOOL)isHidden animated:(BOOL)isAnimated
{
}
- (CGFloat)statusBarHeight
{
return 20;
}
- (BOOL)isInBackground
{
return NO;
}
- (BOOL)isAppForegroundAndActive
{
return YES;
}
- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:
(BackgroundTaskExpirationHandler)expirationHandler
{
return UIBackgroundTaskInvalid;
}
- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)backgroundTaskIdentifier
{
}
- (void)ensureSleepBlocking:(BOOL)shouldBeBlocking blockingObjects:(NSArray<id> *)blockingObjects
{
}
- (void)setMainAppBadgeNumber:(NSInteger)value
{
}
- (nullable UIViewController *)frontmostViewController
{
return nil;
}
- (nullable UIAlertAction *)openSystemSettingsAction
{
return nil;
}
- (void)doMultiDeviceUpdateWithProfileKey:(OWSAES256Key *)profileKey
{
}
- (BOOL)isRunningTests
{
return YES;
}
- (void)setNetworkActivityIndicatorVisible:(BOOL)value
{
}
#pragma mark -
- (void)runNowOrWhenMainAppIsActive:(AppActiveBlock)block
{
block();
}
- (void)runAppActiveBlocks
{
}
- (id<KeychainStorage>)keychainStorage
{
return self.testKeychainStorage;
}
- (NSString *)appDocumentDirectoryPath
{
return self.appDocumentDirPath;
}
- (NSString *)appSharedDataDirectoryPath
{
return self.appSharedDataDirPath;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,15 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@protocol KeychainStorage;
@interface TestKeychainStorage : NSObject <KeychainStorage>
@property (nonatomic) NSMutableDictionary<NSString *, NSData *> *dataMap;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,113 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "TestKeychainStorage.h"
#import "NSData+OWS.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
@implementation TestKeychainStorage
- (instancetype)init
{
self = [super init];
if (!self) {
return self;
}
self.dataMap = [NSMutableDictionary new];
return self;
}
- (NSString *_Nullable)stringForKey:(NSString *)key
service:(NSString *)service
error:(NSError *_Nullable *_Nullable)error
{
OWSAssert(error);
OWSAssert(key.length > 0);
OWSAssert(service.length > 0);
*error = nil;
NSString *mapKey = [NSString stringWithFormat:@"%@-%@", service, key];
NSData *_Nullable data = self.dataMap[mapKey];
if (!data) {
NSLog(@"stringForKey:%@ service:%@ -> nil", key, service);
return nil;
}
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"stringForKey:%@ service:%@ -> %@", key, service, string);
return string;
}
- (BOOL)setWithString:(NSString *)string
forKey:(NSString *)key
service:(NSString *)service
error:(NSError *_Nullable *_Nullable)error
{
OWSAssert(error);
OWSAssert(key.length > 0);
OWSAssert(service.length > 0);
*error = nil;
NSLog(@"setWithString:%@ service:%@ -> %@", key, service, string);
NSString *mapKey = [NSString stringWithFormat:@"%@-%@", service, key];
self.dataMap[mapKey] = [string dataUsingEncoding:NSUTF8StringEncoding];
return YES;
}
- (NSData *_Nullable)dataForKey:(NSString *)key service:(NSString *)service error:(NSError *_Nullable *_Nullable)error
{
OWSAssert(error);
OWSAssert(key.length > 0);
OWSAssert(service.length > 0);
*error = nil;
NSString *mapKey = [NSString stringWithFormat:@"%@-%@", service, key];
NSData *_Nullable data = self.dataMap[mapKey];
NSLog(@"dataForKey:%@ service:%@ -> %@", key, service, data.hexadecimalString);
return data;
}
- (BOOL)setWithData:(NSData *)data
forKey:(NSString *)key
service:(NSString *)service
error:(NSError *_Nullable *_Nullable)error
{
OWSAssert(error);
OWSAssert(key.length > 0);
OWSAssert(service.length > 0);
*error = nil;
NSLog(@"setWithData:%@ service:%@ -> %@", key, service, data.hexadecimalString);
NSString *mapKey = [NSString stringWithFormat:@"%@-%@", service, key];
self.dataMap[mapKey] = data;
return YES;
}
- (BOOL)removeWithKey:(NSString *)key service:(NSString *)service error:(NSError *_Nullable *_Nullable)error
{
OWSAssert(error);
OWSAssert(key.length > 0);
OWSAssert(service.length > 0);
*error = nil;
NSLog(@"removeWithKey:%@ service:%@", key, service);
NSString *mapKey = [NSString stringWithFormat:@"%@-%@", service, key];
[self.dataMap removeObjectForKey:mapKey];
return YES;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -22,6 +22,8 @@ NSString *NSStringForUIApplicationState(UIApplicationState value);
@class OWSAES256Key;
@protocol KeychainStorage;
@protocol AppContext <NSObject>
@property (nonatomic, readonly) BOOL isMainApp;
@ -96,6 +98,12 @@ NSString *NSStringForUIApplicationState(UIApplicationState value);
@property (atomic, readonly) NSDate *appLaunchTime;
- (id<KeychainStorage>)keychainStorage;
- (NSString *)appDocumentDirectoryPath;
- (NSString *)appSharedDataDirectoryPath;
@end
id<AppContext> CurrentAppContext(void);
@ -103,4 +111,8 @@ void SetCurrentAppContext(id<AppContext> appContext);
void ExitShareExtension(void);
#ifdef DEBUG
void ClearCurrentAppContextForTests(void);
#endif
NS_ASSUME_NONNULL_END

View File

@ -43,6 +43,11 @@ void SetCurrentAppContext(id<AppContext> appContext)
currentAppContext = appContext;
}
void ClearCurrentAppContextForTests()
{
currentAppContext = nil;
}
void ExitShareExtension(void)
{
OWSLogInfo(@"ExitShareExtension");

View File

@ -102,17 +102,12 @@ NS_ASSUME_NONNULL_BEGIN
+ (NSString *)appDocumentDirectoryPath
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentDirectoryURL =
[[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
return [documentDirectoryURL path];
return CurrentAppContext().appDocumentDirectoryPath;
}
+ (NSString *)appSharedDataDirectoryPath
{
NSURL *groupContainerDirectoryURL =
[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:SignalApplicationGroup];
return [groupContainerDirectoryURL path];
return CurrentAppContext().appSharedDataDirectoryPath;
}
+ (NSString *)cachesDirectoryPath

View File

@ -5,6 +5,8 @@
#import "ShareAppExtensionContext.h"
#import <SignalMessaging/UIViewController+OWS.h>
#import <SignalServiceKit/OWSStorage.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
#import <SignalServiceKit/TSConstants.h>
NS_ASSUME_NONNULL_BEGIN
@ -212,6 +214,26 @@ NS_ASSUME_NONNULL_BEGIN
OWSFailDebug(@"cannot run main app active blocks in share extension.");
}
- (id<KeychainStorage>)keychainStorage
{
return [SSKKeychainStorage sharedInstance];
}
- (NSString *)appDocumentDirectoryPath
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentDirectoryURL =
[[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
return [documentDirectoryURL path];
}
- (NSString *)appSharedDataDirectoryPath
{
NSURL *groupContainerDirectoryURL =
[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:SignalApplicationGroup];
return [groupContainerDirectoryURL path];
}
@end
NS_ASSUME_NONNULL_END