VOOZH about

URL: https://theapplewiki.com/wiki/Dev:WorkflowKit.framework

⇱ Dev:WorkflowKit.framework - The Apple Wiki


WorkflowKit.framework
Private Framework
Available12.0 – present

WorkflowKit is the framework that acts as a backend to the Shortcuts app. It provides around 80% functionality of the Shortcuts app including (but not limited to) actions (though ActionKit also powers a lot of them), handles how shortcuts are imported, how they're stored, etc. It is noted that while it is technically added in 12.0, most of its functionality came in iOS 13.0. (Some classes from iOS 13.0+ WorkflowKit are similar in the WorkflowAppKit.framework that's embedded in the iOS 12 Shortcuts app).

For examples on how to use this framework, see the Example Code section of this page.

WFWorkflowRecord

WFWorkflowRecord is how shortcuts are stored. The biggest thing is actions: Every action has an action identifier to identify an action (WFWorkflowActionIdentifier), and some have parameters of what is in said action (WFWorkflowActionParameters). However, it also handles other data about the shortcut, such as its name and minimum client version it can be imported on.

WFBundledActionProvider

WFBundledActionProvider is what provides WorkflowKit with what actions it loads. It loads actions from WFActions.plist inside of WorkflowKit - those actions have ActionClass which determine the class of the action (WFBundledActionProvider does NSClassFromString() on the class specified - most of which are in ActionKit.framework), as well as other info.

WFAction

WFAction is basically the class that every shortcuts action uses. It should be noted that actions aren't all WFAction, but rather their own class that inherit from it (For example, WFExitShortcut in ActionKit).

WFShortcutExtractor

WFShortcutExtractor is a brand new class in iOS 15 dedicated to exporting shortcuts.

The -(void)extractShortcutFile:(id)arg0 completion:(id)arg1 method checks if first four characters are AEA1, if so it detects the file being imported as an unsigned shortcut file and calls -(void)extractSignedShortcutFile:(id)arg0 completion:(id)arg1, if not, it detects it as an unsigned shortcut file and calls -(void)extractWorkflowFile:(id)arg0 completion:(id)arg1. -(void)extractWorkflowFile:(id)arg0 completion:(id)arg1 will check for if the allowsOldFormatFile BOOL is true (normally never true), if not, then if it is an internal build of VoiceShortcutsClient, will also check WFShortcutsFileSharingEnabled from the shortcuts preferences plist. If none of these are true it will show an error, but if one is true it calls -(void)extractWorkflowFile:(id)arg0 shortcutName:(id)arg1 shortcutFileContentType:(NSInteger)arg2 iCloudIdentifier:(id)arg3 completion:(id)arg4, with an icloud identifier and shortcut file content type of 0x0. extractSignedShortcutFile determines the shortcut file type and proceeds to call -(void)extractWorkflowFile:(id)arg0 shortcutName:(id)arg1 shortcutFileContentType:(NSInteger)arg2 iCloudIdentifier:(id)arg3 completion:(id)arg4.


WFGallerySessionManager

WFGallerySessionManager handles various gallery things, such as uploading shortcuts to iCloud. Be aware that some methods in here do not have any code in release builds, such as deleteCollection/deleteBanner.

WFP2PSignedShortcutFileExporter

WFP2PSignedShortcutFileExporter is a brand new class in iOS 15 dedicated to signing contact signed shortcuts.

WFiCloudShortcutFileExporter

WFiCloudShortcutFileExporter is a brand new class in iOS 15 dedicated to signing iCloud signed shortcuts. It's noted that while it is new to iOS 15, all it does is upload the shortcut to iCloud using WFGallerySessionManager methods and get the signed shortcut from iCloud; since the WFGallerySessionManager methods also exist on iOS 13/14, this class can easily be backported.

WFShortcutiCloudLinkExporter

WFiCloudShortcutFileExporter is a brand new class in iOS 15 dedicated to signing iCloud signed shortcuts. It has the same functionality as WFiCloudShortcutFileExporter, it just returns an iCloud URL to the shortcut instead of a file URL to the signed shortcut. While it was added in iOS 15+, it can easily be backported to iOS 13/14, check the example code below.

WFShortcutSigningContext

WFiCloudShortcutFileExporter is a brand new class in iOS 15 dedicated to the content of the signed shortcut file. The method -(void)validateAppleIDValidationRecordWithCompletion:(id)arg0 is responsible for verifying that contact-signed shortcuts imported are related to the user or their contacts. It first preforms [[[[[SFAppleIDClient alloc]init]myAccountWithError:nil]altDSID]isEqualToString:[self appleIDValidationRecord]] to check if the DSID inside of the shortcut being imported matches up with the DSID in the shortcut- if not, it checks if private sharing is enabled, and if so, checks that the SHA256 email hash / phone hash in the shortcut match up with any on the user's contacts.

WFShortcutPackageFile

WFShortcutPackageFile is another brand new class in iOS 15. To be honest I'm not 100% knowledgeable on everything this class does, but it appears to handle a lot of AEA handling, as well as some stuff dealing with signing. I should note that its preformShortcutDataExtractionWithCompletion: method (and I could be wrong about this and I just overlooked where it frees it, but I overlooked it a couple times and I can't seem to find the 2nd free() anywhere) is that there is a memory leak, since it only frees a blob when it fails to extract info in a specific way. The generateSignedShortcutFileRepresentationWithAccount:error: method is what generates the private signing key (kSecAttrKeyTypeECSECPrimeRandom) and calls the generateSignedShortcutFileRepresentationWithPrivateKey:signingContext:error: with this.

Example Code

Replacing Return to Homescreen with Exit Shortcut upon shortcut import

%hookWFSharedShortcut
-(WFWorkflowRecord*)workflowRecord{
/* WFWorkflowRecord handles how shortcuts are stored and hooking it will affect every shortcut loaded - not only is this bad for performance, but if you make a mistake, every shortcut will be affected, hence it's best to avoid hooking it when possible. Here, we hook WFSharedShortcut and affect its workflow record instead */
WFWorkflowRecord*workflowRecord=%orig;
NSArray*workflowRecordActions=[workflowRecordactions];
NSMutableArray*newMutableShortcutActions=[workflowRecordActionsmutableCopy];
unsignedintshortcutActionsObjectIndex=0;

for(idshortcutActionsObjectinworkflowRecordActions){
if([shortcutActionsObjectisKindOfClass:[NSDictionaryclass]]){
NSString*actionIdentifier=shortcutActionsObject[@"WFWorkflowActionIdentifier"];
if(actionIdentifier){
/* All shortcuts have an identifier to help identify the action (is.workflow.actions.returntohomescreen is Return to Homescreen, is.workflow.actions.exit is Exit Shortcut). You can find the default action identifiers in /System/​Library/​PrivateFrameworks/​WorkflowKit.framework/WFActions.plist */
if([actionIdentifierisEqualToString:@"is.workflow.actions.returntohomescreen"]){
NSMutableDictionary*mutableShortcutActionsObject=[shortcutActionsObjectmutableCopy];
/* While not demo'ed in this example, actions also have WFWorkflowActionParameters. For example, the file path in Get File is the WFGetFilePath parameter. These can also be found in WFActions.plist, or by extracting an unsigned .shortcut/.wflow file and looking to see the parameters of the action in the shortcut. */

mutableShortcutActionsObject[@"WFWorkflowActionIdentifier"]=@"is.workflow.actions.exit";

newMutableShortcutActions[shortcutActionsObjectIndex]=[[NSDictionaryalloc]initWithDictionary:mutableShortcutActionsObject];
}
}
}
shortcutActionsObjectIndex++;
}

[workflowRecordsetActions:newMutableShortcutActions];
returnworkflowRecord;
}
%end

Iterating through all actions being loaded

%hookWFBundledActionProvider
-(id)createAllAvailableActions{
idcreateactions=%orig;//__NSSetM
for(WFAction*newCreateActionincreateactions){
NSLog(@"Shortcut Action identifier: %@",[newCreateActionidentifier]);
if([[newCreateActionidentifier]isEqualToString:@"is.workflow.actions.exit"]){
// the action being iterated has the action identifier of is.workflow.actions.exit - this means we found the exit shortcut action!
NSLog(@"Exit action: %@",newCreateAction);
}
}

returncreateactions;
}
%end

Importing an unsigned shortcut from a file path (iOS 15+)

WFFileRepresentation*fileRep=[WFFileRepresentationfileWithURL:[NSURLfileURLWithPath:@"/path/to/unsignedShortcut.shortcut"]options:nil];
WFWorkflowFileDescriptor*fileDesc=[[WFWorkflowFileDescriptoralloc]initWithFile:[shortcutExtractorextractingFile]name:@"SnoolieShortcut"];
WFWorkflowFile*wFile=[[WFWorkflowFilealloc]initWithDescriptor:fileDescerror:nil];
WFWorkflowRecord*workflowRecord=[wFilerecordRepresentationWithError:nil];/* requires cloudkit entitlement */
/* now actually add shortcut to database */
WFDatabaseProxy*databaseProxy=[[WFDatabaseProxyalloc]initWithDatabase:[WFDatabasedefaultDatabase]];
[databaseProxycreateWorkflowWithWorkflowRecord:workflowRecordnameCollisionBehavior:0x0error:nil];

(Note: I tested this while injecting into Shortcuts, however if you are using this outside the shortcuts process, then may return NULL since will set it, if you want to initialize it like how handles it without calling it, call which should give you the shortcuts database).

iCloud-Signing an unsigned shortcut (iOS 13+)

WFFileRepresentation*fileRep=[WFFileRepresentationfileWithURL:[NSURLfileURLWithPath:@"/path/to/unsignedShortcut.shortcut"]options:nil];
WFWorkflowFileDescriptor*fileDesc=[[WFWorkflowFileDescriptoralloc]initWithFile:[shortcutExtractorextractingFile]name:@"SnoolieShortcut"];
WFWorkflowFile*wFile=[[WFWorkflowFilealloc]initWithDescriptor:fileDescerror:nil];
WFWorkflowRecord*workflowRecord=[wFilerecordRepresentationWithError:nil];/* requires cloudkit entitlement */
/* now actually sign shortcut */
/*
 * iOS 15 has two classes in relating to iCloud signing:
 * WFiCloudShortcutFileExporter (the one actually used in the macOS CLI tool, outputs signed file)
 * WFShortcutiCloudLinkExporter (outputs iCloud URL)
 * However, both of these are just wrappers around WFGallerySessionManager
 * since its methods exist in iOS 13/14, it can easily be backported to those versions
 * here, I call uploadWorkflow: just like the normal implementation, however
 * the normal implementation of WFiCloudShortcutFileExporter parses the file URL
 * to get the shortcut identifier, then calls getWorkflowForIdentifier
 * I'm not sure what this method does exactly, but it is on iOS 13, however I was worried it
 * might not return signing information on iOS 13, so to be safe, instead I parse the iCloud
 * URL that is returned, and use the iCloud API to retrive the signed shortcut file.
 * Also, note I only tested this when being injected into the Shortcuts process :P.
*/
WFGallerySessionManager*sharedManager=[WFGallerySessionManagersharedManager];
WFFileRepresentation*fileRep=[WFFileRepresentationfileWithURL:[NSURLfileURLWithPath:@"/path/to/unsignedShortcut.shortcut"]options:nil];
[sharedManageruploadWorkflow:workflowRecordwithName:@"Signed Shortcut Test"shortDescription:nillongDescription:nilprivate:YEScompletionHandler:^(NSURL*iCloudURLToShortcut,idexportError){
if(!exportError){
if(iCloudURLToShortcut){
/*
 * iCloudURLToShortcut will be https://www.icloud.com/shortcuts/(shortcutid)
 * We need to get https://www.icloud.com/shortcuts/api/records/(shortcutid)
 * This will have the URL to the signed shortcut file.
 */
NSString*apiURLString=[iCloudURLToShortcut.absoluteStringstringByReplacingOccurrencesOfString:@"/shortcuts/"withString:@"/shortcuts/api/records/"];
NSData*jsonData=[NSDatadataWithContentsOfURL:[NSURLURLWithString:apiURLString]];
NSError*jsonError=nil;
NSDictionary*apiResponse=[NSJSONSerializationJSONObjectWithData:jsonDataoptions:0error:&jsonError];
if(!jsonError){
/* you probably should check for nil with these keys instead of just blindly trusting they all exist but eh, this is just a quick PoC */
NSString*signedShortcutURL=apiResponse[@"fields"][@"signedShortcut"][@"value"][@"downloadURL"];
NSURL*dataGetURL=[[NSURLalloc]initWithString:[signedShortcutURLstringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding]];
NSData*signedShortcutData=[NSDatadataWithContentsOfURL:dataGetURL];
[signedShortcutDatawriteToFile:@"/path/to/output/the/signedShortcut.shortcut"atomically:YES];
}else{
/* handle json error */
NSLog(@"ShortcutsCLI-iOS(jsonError): %@",jsonError);
}
}else{
/* both exportError and iCloudURLToShortcut are nil?? */
NSLog(@"ShortcutsCLI-iOS Error: nil but no err?");
}
}else{
/* Handle errors with iCloud uploading */
NSLog(@"ShortcutsCLI-iOS Error: %@",exportError);
}
}];

Contact-Signing an unsigned shortcut (iOS 15+)

WFFileRepresentation*fileRep=[WFFileRepresentationfileWithURL:[NSURLfileURLWithPath:@"/path/to/unsignedShortcut.shortcut"]options:nil];
WFWorkflowFileDescriptor*fileDesc=[[WFWorkflowFileDescriptoralloc]initWithFile:[shortcutExtractorextractingFile]name:@"SnoolieShortcut"];
WFWorkflowFile*wFile=[[WFWorkflowFilealloc]initWithDescriptor:fileDescerror:nil];
WFWorkflowRecord*workflowRecord=[wFilerecordRepresentationWithError:nil];/* requires cloudkit entitlement */
/* now actually sign shortcut */
WFP2PSignedShortcutFileExporter*contactExporter=[[WFP2PSignedShortcutFileExporteralloc]initWithWorkflowRecord:workflowRecord];
[contactExporterexportWorkflowWithCompletion:^(idarg0,idarg1){
/*
 * What you *should* do, and what the normal cli
 * for macOS does is to use the result from
 * the completion method. I'm not for contact
 * signed, but however contact signed shortcuts
 * should store the result in the property
 * afterwards anyway, which I'm just using
 * that here, so it should be fine.
 */
NSLog(@"Completion block called (not used here)");
}];
/* This is fine (for contact signed) */
WFFileRepresentation*signedShortcutFile=[contactExportersignedShortcutFile];
NSData*signedShortcutFileData=[signedShortcutFiledata];
[signedShortcutFileDatawriteToFile:someFileURLToOutputSignedShortcutatomically:YES];

iCloud-Signing an unsigned shortcut using WFiCloudShortcutFileExporter (iOS 15+)

WFFileRepresentation*fileRep=[WFFileRepresentationfileWithURL:[NSURLfileURLWithPath:@"/path/to/unsignedShortcut.shortcut"]options:nil];
WFWorkflowFileDescriptor*fileDesc=[[WFWorkflowFileDescriptoralloc]initWithFile:[shortcutExtractorextractingFile]name:@"SnoolieShortcut"];
WFWorkflowFile*wFile=[[WFWorkflowFilealloc]initWithDescriptor:fileDescerror:nil];
WFWorkflowRecord*workflowRecord=[wFilerecordRepresentationWithError:nil];/* requires cloudkit entitlement */
/* now actually sign shortcut */
WFiCloudShortcutFileExporter*icloudExporter=[[WFiCloudShortcutFileExporteralloc]initWithWorkflowRecord:workflowRecord];
[icloudExporterexportWorkflowWithCompletion:^(idfileURLToiCloudSignedShortcut,idexportError){
/*
 * Unlike contact-signed, we *need* to use
 * the completion block here. While
 * WFiCloudShortcutFileExporter does also
 * have a signedShortcutFile property,
 * either it's never set, or trying to
 * get it right after can cause some race
 * conditions (which juding by how
 * [icloudExporter signedShortcutFile];
 * seemed to be called after the completion
 * block was called to my testing, it very
 * well could be race conditions). The
 * first arg will be a file:/// URL to
 * a temp directory storing the signed shortcut
 * file. exportError will be null unless there's
 * an error. If there's an error, the first arg
 * will be null.
 */
NSLog(@"Completion block called");
if(!exportError){
/* do cool stuff */
}else{
/* Handle errors */
NSLog(@"ShortcutsCLI-iOS Error: %@",exportError);
}
}];

iOS 13/14 Backport of WFShortcutiCloudLinkExporter

@interface WFShortcutiCloudLinkExporter : NSObject
@property(nonatomic,readonly)WFWorkflowRecord*workflowRecord;
-(void)exportWorkflowWithCompletion:(id)completion;
-(instancetype)initWithWorkflowRecord:(WFWorkflowRecord*)workflowRecord;
-(instancetype)initWithWorkflowRecord:(WFWorkflowRecord*)workflowRecordsharingOptions:(id)argWeDontCareAbout;
@end
@implementation WFShortcutiCloudLinkExporter
-(instancetype)initWithWorkflowRecord:(WFWorkflowRecord*)workflowRecord{
self=[superinit];
if(self){
_workflowRecord=workflowRecord;
}
returnself;
}
-(instancetype)initWithWorkflowRecord:(WFWorkflowRecord*)workflowRecordsharingOptions:(id)argWeDontCareAbout{
/* the normal implementation of this in WFShortcutExporter calls prepareForSharingWithOptions but for just backporting WFShortcutiCloudLinkExporter we dont care about that */
return[selfinitWithWorkflowRecord:workflowRecord];
}
-(void)exportWorkflowWithCompletion:(id)completion{
WFGallerySessionManager*sharedManager=[WFGallerySessionManagersharedManager];
WFWorkflowRecord*workflowRecord=[selfworkflowRecord];
/* Here's a fun fact! This backport should be a bit more optimized than the normal way (though it's so minor that you'll see no difference) as the actual implementation in iOS 15 calls [self workflowRecord] twice since it recalls it to get the name, but this implementation only calls it once */
[sharedManageruploadWorkflow:workflowRecordwithName:[workflowRecordname]shortDescription:nillongDescription:nilprivate:YEScompletionHandler:completion];
}
@end

libshortcutsign

libshortcutsign is a library by Snoolie K / 0xilis that replicates WorkflowKit's behavior for contact signing, allowing you to contact sign a shortcut without needing to link WorkflowKit (assuming you have already managed to extract your Apple ID Validation Record certificates; you may want to use https://github.com/seemoo-lab/airdrop-keychain-extractor ), and even sign in a regular jailed application. It also allows you to extract the auth data from a contact signed shortcut, extract/decrypt an unsigned shortcut from a contact signed shortcut, and verify a contact signed shortcut. Be aware if you want to use it to sign, you'll need to construct the auth data yourself; you can try extracting the auth data from another contact signed shortcut to get a better understanding. Another helpful resource you may also want to look at the decompilation for WorkflowKit shortcut signing, especially and in it; WorkflowKit uses to generate the auth data which you can find in the decomp. Directly copy and pasting the decomplication shouldn't be done, at least for public projects as it might get you into some hot water with Apple's legal team, but it is great to use as a reference for how you could build your own method to do this. Probably the best resource though would be Snoolie K's paper on contact signed shortcuts, https://github.com/0xilis/blog/blob/main/shortcuts/ReversingContactSignedShortcuts.md. You can find libshortcutsign here: https://github.com/0xilis/libshortcutsign.

References