Sharing secure files from an extension

An AppConnect app can provide an app extension, specifically a Document View Controller extension, to share secure files with other AppConnect apps. A file can be shared with all AppConnect apps or with only specific AppConnect apps.

NOTE: To share secure files between AppConnect apps, see Secure file I/O API details.

Sharing secure documents from an extension requires the following tasks:

Setting up the MobileIron server for sharing files from an extension
Setting up the provider app’s Info.plist
Coding the provider app to share secure files with its extension
Coding the extension to share files with the host app
Coding the host app to access the shared file

The sample app SwiftFileSharing illustrates coding these tasks in Swift.

Setting up the MobileIron server for sharing files from an extension

If you want your AppConnect app’s extension to share secure files with other AppConnect apps, define values for the keys MI_AC_SHARED_GROUP_ID and MI_AC_ACCESS_CONTROL_ID. In the documentation that you provide to the MobileIron server administrator about your AppConnect, include:

the values you define
the AppConnect apps that you want to use your extension to access the secure files

The server administrator sets the key-value pairs in the app configuration of your app and each AppConnect app that is to share the secure files. If the server administrator does not set MI_AC_SHARED_GROUP_ID, then all AppConnect apps can access the shared secure files.

NOTE: In the MobileIron Core Admin Portal, app key-value pairs are set up in Policy & Configs > Configurations, in the App-specific Configurations section of an AppConnect App Configuration. In the MobileIron Cloud Admin Portal, the key-value pairs are set up in the AppConnect Custom Configuration section of the app.

Setting up the provider app’s Info.plist

For a provider app to share secure files through its extension, do the following:

1. Include the following key-value pairs in the app’s Info.plist:
- MI_APP_CONNECT

This key is the root key, and its value is a dictionary of key-value pairs

- MI_AC_KEYCHAIN_ACCESS_GROUP

This key provides a keychain access group that the AppConnect library uses to share secure files between the provider app and its extension. The value is the app’s identifier prefix followed by a string you define.

For example:

2. In the Xcode project, in Capabilities > Keychain Sharing, add the string you defined. In this example, the string to add is com.mycompany.MyACSharedFiles.

Coding the provider app to share secure files with its extension

The following sample code illustrates the AppConnect APIs that the provider app uses to share secure files with its extension. The sample code is followed by a table of the tasks involved.

// When the AppConnect isReady notification is triggered, enable extension support.
-(void)appConnectIsReady:(AppConnect *)appConnect {
	[[AppConnect sharedInstance] enableAppExtensionSupport];
}
 
// Insert code to use the read-only config property on the AppConnect singleton to 
// get the MI_AC_SHARED_GROUP_ID key-value pair, if available. 
 
// In this example, the key-value pair was not included, so nil 
// is passed to -getCryptoKeysForACFileEncryptionWithSharedGroupID:error: for the group ID.
 
// When secure services are available, create an encryption key for encrypting secure files.
 
-(void)appConnect:(AppConnect *)appConnect 
	secureServicesAvailabilityChangedTo:(ACSecureServicesAvailability)secureServicesAvailability {
 
	if (secureServicesAvailability == ACSECURESERVICESAVAILABILITY_AVAILABLE) {
        	NSData *secureKeyData = 
		   [ACWrappedAppKey getCryptoKeysForACFileEncryptionWithSharedGroupID:nil error:nil];
 
		// The secureKeyData object contains the encryption key.
		// Store the secureKeyData object in a shared keychain that the extension
		// can access.
	}
}
Table 1. Coding the provider app to share secure files with its extension

Task

AppConnect APIs

1. Enable extension support.

Call the -enableAppExtensionSupport: method on the AppConnect singleton object.

Header

AppConnectInterface.h
2. Get the value of the MI_AC_SHARED_GROUP_ID key-value pair.

Use the read-only config property on the AppConnect singleton to get the MI_AC_SHARED_GROUP_ID key-value pair, if available.

App-specific configuration API details
AppConnectInterface.h
3. Make sure secure services are available.

Check if the secureServicesAvailability property on the AppConnect singleton has the value ACSECURESERVICESAVAILABILITY_AVAILABLE.

Continue only if secure services are available

Secure services API details
AppConnectInterface.h
4. Create an encryption key for encrypting shared files.

Method 

+(NSData *)
   getCryptoKeysForACFileEncryptionWithSharedGroupID:(NSString *)groupID
     error:(NSError *_autoreleasing *)error;

Parameters 

groupID

Pass the value of the MI_AC_SHARED_GROUP_ID key-value pair. If this key-value pair is not available, pass nil. Passing nil means that all AppConnect apps can decrypt the shared file.

error

If the method fails to create an encryption key, error is set to the appropriate NSError object.

Return value 

NSData object containing key used for shared file encryption

Header file 

ACWrappedAppKey.h
5. Store the returned encryption key in a shared keychain item used by the provider app and its extension.

 

Coding the extension to share files with the host app

The following sample code illustrates what the Document View Controller extension does to share secure files with a host app. The sample code is followed by a table of the tasks involved.

// Add the following ExtensionManager class to your extension code. Your extension will  
// create a singleton instance of the class, which takes care of all the 
// AppConnect-related operations.
 
@class ExtensionManager;
 
@protocol ExtensionManagerProtocol
-(void)extensionManager:(ExtensionManager *)extensionManager 
			appConnectAccessControlStateDeterminedAs:(ACExtensionAccessState)state;
@end
 
@interface ExtensionManager: NSObject <AppConnectExtensionInterfaceProtocol>
@property (weak) AppConnectExtensionInterface *acInterface;
@property (weak) id<ExtensionManagerProtocol> delegate;
@end
 
@implementation ExtensionManager
 
+(instancetype)sharedInstance {
	static ExtensionManager *sharedInstance = nil;
	static dispatch_once_t onceToken;
	dispatch_once(&onceToken, ^{
	    sharedInstance = [[ExtensionManager alloc] init];
	    sharedInstance.acInterface = [AppConnectExtensionInterface appConnectExtensionInstance];
	    sharedInstance.acInterface.delegate = sharedInstance;
	});
	return sharedInstance;
}
 
-(void)requestAccessControlState {
	// Initiate a process that determines whether the host app is allowed to 
	// access the extension. 
        [self.acInterface determineAccessControlState];
}
 
-(void)appConnectAccessControlStateDeterminedAs:(ACExtensionAccessState)state {
    [self.delegate extensionManager:self appConnectAccessControlStateDeterminedAs:state];
}
 
@end
 
 
//
//
// In your UIDocumentPickerExtensionViewController implementation, include the following code:
//
//
 
-(void)prepareForPresentationInMode:(UIDocumentPickerMode)mode {
 
        // Insert code to present a view controller appropriate for the picker mode.
        // Then...
 
    switch (mode) {
	case UIDocumentPickerModeOpen:
	case UIDocumentPickerModeImport:
 
		ExtensionManager *extensionManager = [ExtensionManager sharedInstance];
		[extensionManager setDelegate:self];
		[extensionManager requestAccessControlState];
 
		// Start a spinner while waiting to find out if the host app is allowed
		// to access the extension.
		[_spinner startAnimating];
    }
}
 
//
//
// Your UIDocumentPickerExtensionViewController class implements 
// the ExtensionManagerProtocol.
//
 
-(void)extensionManager:(ExtensionManager *)extensionManager 
			    appConnectAccessControlStateDeterminedAs:(ACExtensionAccessState)state {
	currentState = state;
	[_spinner stopAnimating];
 
	switch (currentState) {
 
		case ACExtensionAccessStateNoRequest:
			// A non-AppConnect App has launched the extension. 
			// Do not share the file. Take necessary steps, such as notifying the user. 
 			break;
 
		case ACExtensionAccessStateNotEnabled:
			// Either the administrator did not configure MI_AC_ACCESS_CONTROL_ID for
			// the provider app, or the provider app has not setup access control by 
			// calling -enableAppExtensionSupport:. 
			// Do not share the file. Take necessary steps, such as notifying the user.
			break;
 
		case ACExtensionAccessStateBlocked:
			// The host app does not have access to this extension. It does not have
			// the same MI_AC_ACCESS_CONTROL_ID as the provider app.
			// Do not share the file. Take necessary steps, such as notifying the user.		
			break;
 
		case ACExtensionAccessStateNotBlocked:
			// Share the wrapped file. An AppConnect app has launched the extension.
			break;
	}
}
Table 2. Coding the Document View Controller extension to share files with the host app

Task

AppConnect APIs and sample code

1. Define an ExtensionManager class that is derived from NSObject and implements the AppConnectExtensionInterfaceProtocol.

The sample code provides a full implementation of an ExtensionManager class that you can use.

It includes the implementation of:

the AppConnectExtensionInterface method:

-(BOOL) determineAccessControlState;

 

the AppConnectExtensionInterfaceProtocol callback method:
-(void) appConnectAccessControlStateDeterminedAs:
          (ACExtensionAccessState)state;

Header file

AppConnectExtensionInterface.h in the AppConnectExtension.framework
2. Your
UIDocumentPickerExtensionViewController class implements the ExtensionManagerProtocol.

The sample code explains the handling of each ACExtensionAccessState value in the ExtensionManagerProtocol callback method. The app continues to file sharing processing only if the value is ACExtensionAccessStateNotBlocked.

 

3. Your
UIDocumentPickerExtensionViewController object does the following:
- Creates a singleton instance of the ExtensionManager class.
- Sets the ExtensionManager’s delegate so that you can receive the callback.
- Initiates the request to determine if the host app is allowed to use the extension.

The sample code shows this sequence in
-prepareForPresentationInMode:.

4. When the ExtensionManagerProtocol callback method is called with state set to ACExtensionAccessStateNotBlocked, read the encryption key stored as an NSData object in the shared keychain item.

 

5. Wrap the selected file using the encryption key.

Method 

+(BOOL)wrapFileAtPath:(NSString *)path
    toPath:(NSString *)toPath
       withCryptoBlock:(NSData *)cryptoBlock
       actualFileName:(NSString *fileName)
       error:(NSError *_autoreleasing *)error;

Parameters 

path

Pass the file URL of the selected file.

toPath

Pass the file URL of where the resulting wrapped file should be stored.

cryptoBlock

Pass the NSData object containing the encryption key.

actualFileName

Optional. File name for the wrapped file, if it should be different than the original file name.

error

If the method fails, error is set to the appropriate NSError object.

Return value 

YES if successful. Otherwise NO.

Header file

ACWrappedFile.h in the AppConnectExtension.framework

Coding the host app to access the shared file

The following sample code illustrates what the host app does to access the secure file shared by the extension. The sample code is followed by a table of the tasks and header files involved.

// Insert code to use the read-only config property on the AppConnect singleton to 
// get the MI_AC_SHARED_GROUP_ID key-value pair, if available. 
// In this example, the key-value pair was not included. Therefore, nil 
// is passed for the group ID parameter to -readWrappedFileAtPath:sharedGroupID:error:, 
// which gets the file handle of the shared, wrapped file.
 
-(NSURL *)getDecryptedFileURL:(NSURL *)url {
 
	ACWrappedFileReadHandle *readHandle = [ACUnwrappedFile readWrappedFileAtPath:url.path 				
			                                       sharedGroupID:nil error:&error];
	if (readHandle) {
 
		NSFileHandle *writeToFileHandle = 
                    [NSFileHandle fileHandleForWritingAtPath:decURL.path];
 
		// Decrypt the file by reading it with the ACWrappedFileReadHandle object.
		// This snippet then writes it to an unencrypted file.
 
		NSData *decryptedData = nil;
		while ((decryptedData = 
                       [readHandle readDataOfLength:1024]) && (decryptedData.length > 0)) {
 
			  [writeToFileHandle writeData:decryptedData];
		}
 
		[writeToFileHandle synchronizeFile];
		[writeToFileHandle closeFile];
 
		// You can remove the wrapped file after decrypting it.
		[[NSFileManager defaultManager] removeItemAtURL:url error:nil];
		return decURL;
	}
	else if (error && error.code == ACWrappedFileReadErrorUnknownWrapperFormat) {
 
		// The file is not wrapped. It is not from an AppConnect app’s extension.
		// It can be used directly.
		return url;
	}
}
Table 3. Coding the host app to access the shared file

Task

AppConnect APIs

1. Get the value of the MI_AC_SHARED_GROUP_ID key-value pair.

Use the read-only config property on the AppConnect singleton to get the MI_AC_SHARED_GROUP_ID key-value pair, if available.

App-specific configuration API details
AppConnectInterface.h
2. Get the file handle of the shared, wrapped file.

Method 

+(ACWrappedFileReadHandle *) readWrappedFileAtPath:(NSString *)path
    sharedGroupID:(NSString *)groupID
    error:(NSError *__autoreleasing *)error;

Parameters 

path

Pass the file URL of the file returned from the extension.

sharedGroupID

Pass the value of the MI_AC_SHARED_GROUP_ID key-value pair. If this key-value pair is not available, pass nil.

error

If the method fails, error is set to the appropriate NSError object.

Return value 

If successful, returns the file handle of the shared, wrapped file as a ACWrappedFileReadHandle object. Otherwise, returns nil.

Header files

ACUnwrappedFile.h in the AppConnect.framework
ACWrappedFileReadHandle.h in the AppConnect.framework
3. Using the file handle, read and decrypt the file’s contents.

Methods 

Use the methods in ACFlleHandle.h to read and decrypt the file’s contents.