Using StoreKit from an AUv3 plugin that can be loaded in-process

I have a bunch of Audio Unit v3 plugins that are approaching release, and I was considering using subscription-model pricing, as I have done in a soon to be released iOS app. However, whether this is possible or not is not at all obvious. Specifically:

  • The plugin can, depending on the host app, be loaded in-process or out-of-process - yes, I know, Logic Pro and Garage Band will not load a plug-in in-process anymore, but I am not going to rule that out for other audio apps and force on them the overhead of IPC (I spent two solid weeks deciphering the process to actually make it possible for an AUv3 to run in-process - see this - https://github.com/timboudreau/audio_unit_rust_demo - example with notes)
  • Depending on how it is loaded, the value of Bundle.main.bundleIdentifier will vary. If I use the StoreKit API, will that return product results for my bundle identifier when being called as a library from a foreign application? I would expect it would be a major security hole if random apps could query about purchases of other random apps, so I assume not.
  • Even if I restricted the plugins to running out-of-process, I have to set up the in-app purchases on the app store for the App container's ID, not the extension's ID, and the extension is what run - the outer app that is what you purchase is just a toy demo that exists solely to register the audio unit.

I have similar questions with regard to MetricKit, which I would similarly like to use, but which may be running inside some random app.

If there were some sort of signed token, or similar mechanism, that could be bundled or acquired by the running plugin extension that could be used to ensure both StoreKit and MetricKit operate under the assumption that purchases and metrics should be accessed as if called from the container app, that would be very helpful.

This is the difference between having a one-and-done sales model and something that provides ongoing revenue to maintain these products - I am a one-person shop - if I price these products where they would need to be to pay the bills assuming a single sale per customer ever, the price will be too high for anyone to want to try products from a small vendor they've never heard of. So, being able to do a free trial period and then subscription is the difference between this being a viable business or not.

Answered by DTS Engineer in 878878022
If there were some sort of signed token, or similar mechanism, that could be bundled or acquired by the running plugin extension

You should feel free to file an enhancement request for that, but I want to set some expectations here [1]. Apple systems enforce security at process boundaries. If such an API existed:

  • It’d be impossible for the OS to determine whether you’re plug-in made this call, as opposed to some other code in the host app process.
  • Once you had such a token, it’d be impossible for you to prevent some other code in the host app process from ‘stealing’ it [2].

Keep in mind that this “other code” isn’t just the host app itself, but it also includes:

  • Any other plug-in that the host app might load
  • Any code ‘accidentally’ loaded by the host app or these plug-ins [3]

The long-term direction here is clear: To meet platform security goals plug-ins must run in a separate process. This is strictly enforced on iOS and it’s generally a good idea on macOS [4]. If you’re dealing with an host app that only supports in-process plug-ins, I recommend that you discuss this issue with them.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] If you do file an ER, please post your bug number, just for the record.

[2] You could apply various code obfuscation techniques to reduce the risk of that, but that’s essentially creating a DRM system. And as with all DRM systems, it puts you into an arms race with your attackers.

[3] Because the host app must necessarily have library validation disabled.

[4] I want to be clear that Apple has not announced any plans to enforce this strictly on macOS. There are obvious security benefits, but there are also equally obvious compatibility constraints.

If there were some sort of signed token, or similar mechanism, that could be bundled or acquired by the running plugin extension

You should feel free to file an enhancement request for that, but I want to set some expectations here [1]. Apple systems enforce security at process boundaries. If such an API existed:

  • It’d be impossible for the OS to determine whether you’re plug-in made this call, as opposed to some other code in the host app process.
  • Once you had such a token, it’d be impossible for you to prevent some other code in the host app process from ‘stealing’ it [2].

Keep in mind that this “other code” isn’t just the host app itself, but it also includes:

  • Any other plug-in that the host app might load
  • Any code ‘accidentally’ loaded by the host app or these plug-ins [3]

The long-term direction here is clear: To meet platform security goals plug-ins must run in a separate process. This is strictly enforced on iOS and it’s generally a good idea on macOS [4]. If you’re dealing with an host app that only supports in-process plug-ins, I recommend that you discuss this issue with them.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] If you do file an ER, please post your bug number, just for the record.

[2] You could apply various code obfuscation techniques to reduce the risk of that, but that’s essentially creating a DRM system. And as with all DRM systems, it puts you into an arms race with your attackers.

[3] Because the host app must necessarily have library validation disabled.

[4] I want to be clear that Apple has not announced any plans to enforce this strictly on macOS. There are obvious security benefits, but there are also equally obvious compatibility constraints.

Thanks. That was about the answer I expected re the ability to run in-process (though I'll note that the industry has produced plenty of ways to run untrusted code in-process, from Java's class loaders to the handful of system calls it takes on Linux to spin up an isolated container within a process).

However, it did not address the more fundamental question of, can using StoreKit in an AUv3 work at all?

Say I have an app, shipped via the App Store, with the bundle ID com.mycom.Floofifier, containing an extension com.mycom.Floofifier.FloofifierExtension - which is the thing that will actually be executed by the host.

In-app purchases are - must be - registered against the ID of the outer application, which is essentially never run.

Will calling Product.products(for: ...) return anything usable when the process is com.mycom.Floofifier.FloofifierExtension not the outer app?

the industry has produced of ways to run untrusted code in-process

Sure. And if the only issue were the host not being able to trust the guest, such techniques might be feasible. But in this case you have to worry about the guest not trusting the host.

can using StoreKit in an AUv3 work

There’s an obvious way to make this work:

  1. Have the user run the app.
  2. Which uses StoreKit to check for the purchase.
  3. And stores the state in an app group, or keychain access group [1].
  4. Which the app extension checks.

This requires that the user run the app at least once so that it can achieve, but that’s not particularly onerous.

However, the real question is really about whether your app extension can call StoreKit. I don’t know for sure. I’m gonna research this and get back to you.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] A keychain access group requires you to use the data protection keychain. See TN3137 On Mac keychain APIs and implementations for more about that. One advantage of the data protection keychain is that its access control is mediated by entitlements. So someone can’t just write their own program to manipulate that state.

However, this doesn’t make it 100% secure. For example:

  • Disabling SIP undermines entitlement enforcement.
  • The user can see some types of keychain items in Keychain Access.

Yeah, the "obvious" way doesn't work so well if you're using auto-renewing subscriptions - forcing the user to run the outer app every three days when the grace period expires to see if the user is still subscribed is a non-starter - this needs to be transparent and just work™ once the user has bought the subscription - my target customers are mastering engineers and similar - I wouldn't put up with that from a plugin for long, so I cannot expect my customer to.

I have one local beta tester I'm going to try a TestFlight version with subscription support enabled and lots of logging and see if the extension is able to see "products" defined for the outer app.

The fact that all of this is essentially untestable by the developer (getting a StoreKit config through to the extension process seems to be a no-go, not to mention my experience with local StoreKit configs working on a much simpler iOS app spotty enough not to trust) is fairly maddening. The combinatorics in the state machine you need for definitely-not-subscribed, subscribed +/- verified | pending, storekit-error plus persisted-from-a-previous-session versions of those, plus in-initial-install-grace-period and in-period-after-a-successful-subscription-check-when-we-don't-check-again (the plugin should work for 3 days after a successful subscription check, offline) get pretty substantial, and screwing up any combination can lock the user out of something they have purchased. I will probably ship with subscription support if we find it can work at all, but I am well and truly flying blind. Not to mention all of this having to work its transparent magic without ever stepping in the path of a real-time audio thread.

Just to make this a bit more real, https://mastfrog.com/dsp/ shows the (at-the-moment vaporware) set of plugins - Mac OS and iOS versions.

Earlier I wrote:

the … question is really about whether your app extension can call StoreKit

I’ve confirmed that we don’t support StoreKit in app extensions [1].

At this point I don’t see a good solution. You can certainly file an enhancement request explaining your use case and requesting that we support it, but that won’t help in the short term.

Note If you do file an ER, please post your bug number, just for the record.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Apparently there’s an exception for iMessage extensions, but that doesn’t help you.

Well, there goes my business model.

Any thoughts on workarounds? Like, app-extension launches outer app without opening a GUI, communicates over a socket (or whatever) and does the storekit, app does store kit check and can open the GUI to buy if needed?

Seems pretty byzantine and fragile, but might be doable, depending on the app extension's ability to launch the outer app.

Seems pretty byzantine and fragile

Yeah. I considered this approach before I replied to you yesterday, but I concluded that:

  • I’m not sure if it’s possible to pull off [1].
  • And even if it is, it’s likely to be very fragile.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Your appex is sandboxed so:

  • It can’t spawn your app’s executable as a child process because that would require reinitialising the sandbox, something that the system specifically blocks. See App Sandbox Inheritance within Resolving Trusted Execution Problems.
  • It should be able to launch the app with NSWorkspace, but NSWorkspace doesn’t honour all of its configuration when it’s called by a sandboxed process.

Ugh. The only remaining thing I can think of is to have, say, a completely separate "license manager" application responsible for all subscriptions to the set of plugins I'm selling, and using something like mach ports to communicate with it. And I suspect that would (aside from being a pretty alien way of doing things for Mac users, at least unless you use Adobe products) probably run afoul of one or another App Store policy (would it? Adobe is certainly a precedent, though I lack their clout).

BTW, however I solve this, if I do, it needs to work on both Mac OS and iOS - I have no idea if there's much market for iOS Audio Units, but it was easy enough to build for both, so I will release both. It's at least likely to be a less saturated market :-)

that would … probably run afoul of one or another App Store policy

I don’t work for App Review, and thus can’t give definitive answers about their policies. My only advice is that you peruse the App Review Guidelines.

At a technical level, on macOS it is possible for an app extension to communicate with an app or launchd agent or daemon that you control, even in the presence of sandboxing. If you want the server side of this to be an app, the best option is for that app to listen on a Unix domain socket that’s located in an app group container. If you want it to be a launchd job, prefix the named XPC endpoint with an app group ID. Both of these are documented in App Groups Entitlement.

For more on the XPC side of this, see the various links in XPC Resources. Specifically, XPC and App-to-App Communication has a bunch of backstory you’ll need.

And please don’t use Mach messaging directly!

it needs to work on both Mac OS

This isn’t really feasible on iOS. XPC is not an option because third-party apps can’t publish named XPC endpoints [1]. And while a Unix domain socket in an app group is supported, things fall over as soon as the listener process moves to the background and gets suspended.

I talk about this more in iOS Background Execution Limits.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] XPC is present on iOS and can be used for a couple of specific IPC scenarios, but none of those present a general mechanism that you can use to solve this problem.

Using StoreKit from an AUv3 plugin that can be loaded in-process
 
 
Q