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:
-
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:
-
-
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. } }
Task |
AppConnect APIs |
|||||||||||||||
|
Call the -enableAppExtensionSupport: method on the AppConnect singleton object. Header
|
|||||||||||||||
|
Use the read-only config property on the AppConnect singleton to get the MI_AC_SHARED_GROUP_ID key-value pair, if available.
|
|||||||||||||||
|
Check if the secureServicesAvailability property on the AppConnect singleton has the value ACSECURESERVICESAVAILABILITY_AVAILABLE. Continue only if secure services are available
|
|||||||||||||||
|
Method +(NSData *) getCryptoKeysForACFileEncryptionWithSharedGroupID:(NSString *)groupID error:(NSError *_autoreleasing *)error; Parameters
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.
If the method fails to create an encryption key, error is set to the appropriate NSError object. Return value
Header file
|
|||||||||||||||
|
|
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; } }
Task |
AppConnect APIs and sample code |
||||||||||||||||||||||||
|
The sample code provides a full implementation of an ExtensionManager class that you can use. It includes the implementation of:
-(BOOL) determineAccessControlState;
-(void) appConnectAccessControlStateDeterminedAs: (ACExtensionAccessState)state; Header file
|
||||||||||||||||||||||||
|
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.
|
||||||||||||||||||||||||
|
The sample code shows this sequence in |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
Method +(BOOL)wrapFileAtPath:(NSString *)path toPath:(NSString *)toPath withCryptoBlock:(NSData *)cryptoBlock actualFileName:(NSString *fileName) error:(NSError *_autoreleasing *)error; Parameters
Pass the file URL of the selected file.
Pass the file URL of where the resulting wrapped file should be stored.
Pass the NSData object containing the encryption key.
Optional. File name for the wrapped file, if it should be different than the original file name.
If the method fails, error is set to the appropriate NSError object. Return value
Header file
|
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; } }
Task |
AppConnect APIs |
|||||||||||||||||||||
|
Use the read-only config property on the AppConnect singleton to get the MI_AC_SHARED_GROUP_ID key-value pair, if available.
|
|||||||||||||||||||||
|
Method +(ACWrappedFileReadHandle *) readWrappedFileAtPath:(NSString *)path sharedGroupID:(NSString *)groupID error:(NSError *__autoreleasing *)error; Parameters
Pass the file URL of the file returned from the extension.
Pass the value of the MI_AC_SHARED_GROUP_ID key-value pair. If this key-value pair is not available, pass nil.
If the method fails, error is set to the appropriate NSError object. Return value
Header files
|
|||||||||||||||||||||
|
Methods Use the methods in ACFlleHandle.h to read and decrypt the file’s contents. |