The suspiciously-good UI of BIAS FX 2

When trying out different guitar rig plugins, I came across BIAS FX 2. I liked the tones I could create with it, but this writeup actually isn’t about that. One thing that really impressed me was the quality of the plugin’s UI.


Most audio plugin UIs have always been a step behind other modern UIs. This isn’t completely surprising; the popular UI frameworks for audio plugins are either primitive in their styling options, or require lots of effort to make look good. Knowing this, I was very impressed by how pleasant BIAS FX’s UI was; the animations were smooth, theming was consistent, and things like drag-and-drop felt perfectly natural. I was curious how they pulled it off.

Too good to be true

As it would turn out, the answer wasn’t some magical, proprietary UI framework: it was HTML and CSS. This isn’t surprising in 2023—even decades-old apps like Photoshop now have portions of their UI that are just embedded web views. However, I’ve never seen this strategy used in an audio plugin before. I’m not certain BIAS FX is the first to do this, but it’s the first I’ve come across, which made it interesting to me.

You wouldn't put a web browser in a VST plugin meme

Surprisingly, rather than effectively shipping a copy of Chromium, the platform-native browser engine is used to drive the web view (at least on macOS). This keeps installation size down, and as a bonus, yields a smoother experience given how well-optimized WebKit is for Apple hardware/platforms.

Nothing to see here

Knowing the UI is really just a website explains a lot about about its fidelity; as such, my focus shifted to peeking at the website sources and assets directly. Looking through the plugin’s resources directory reveals no shortage of HTML/CSS/JS files. Some of these look normal, but others (such as index.html) look more like this:

$ xxd index.html | head -n 10
00000000: 5047 3030 3030 3030 2d30 3030 302d 3030  PG000000-0000-00
00000010: 3030 2d30 3030 302d 3030 3030 3030 3030  00-0000-00000000
00000020: 3030 3031 0500 0000 3136 3830 37c5 0200  0001....16807...
00000030: 0017 763a f864 b48c c5be 2e3e 7932 4642  ..v:.d.....>y2FB
00000040: 5798 5198 32a2 d78f 2c0b 868b 60b2 d93e  W.Q.2...,...`..>
00000050: 9815 9a38 2588 5a32 7dca a32a ca8b e55b  ...8%.Z2}..*...[
00000060: 71a7 9ef8 6856 9a10 f63d 8145 b56c 1af4  q...hV...=.E.l..
00000070: 5e86 0d57 44c1 70ac 325c c1cd 97e8 3536  ^..WD.p.2\....56
00000080: 9933 1fd7 5a0e 81dc f4a5 0135 ac76 c9cc  .3..Z......5.v..
00000090: f2d5 edf0 cdf4 500f 6c67 167e 6614 63c9  ......P.lg.~f.c.

Clearly these files are in some encoded format. Comparing a handful of these encoded files visually shows they all share an apparent structure: a GUID followed by “garbage”. The high entropy of these files suggests the “garbage” is likely compressed or encrypted data:

$ rahash2 -a entropy index.html
index.html: 0x00000000-0x00000300 entropy: 7.66375743

WTF: What’s the format?

Spoiler alert: it’s not compression. Some reversing of the plugin tells that these files are a thin container over an encrypted payload. Represented as a C type, the format looks like this:

struct EncodedFile __attribute((packed)) {
  int8_t magic_guid[36];          /* PG000000-0000-0000-0000-000000000001 */
  int32_t salt_len;               /* 0x5 */
  int8_t salt[salt_len];          /* 16807 */
  int32_t payload_len;            /* 0x2c5 */
  uint8_t payload[payload_len];   /* ... */

Every file begins with the magic GUID shown above. Since the plugin loads all files through the same code path—and not all files are encrypted—this GUID is used as a flag to indicate if the file needs decryption before its contents are returned to the calling code.

Following the GUID is a 32-bit integer containing the length of the “salt”, a numerical ASCII string used in deriving the encryption key for the file. Finally, there is another 32-bit integer holding the length of the encrypted payload, followed by the payload itself.

Encryption begone

More reversing shows that the payload itself is encrypted using AES-256 in ECB mode. Each file has a unique key derived from a constant “prefix” string and the salt in the preamble. The steps for deriving the key are as follows:

  seed = Concatenate(salt_prefix, salt)
part_a = MD5(SHA1(seed))
part_b = SHA1(MD5(seed))
   key = Truncate(Concatenate(part_a, part_b), 32)

To summarize in English, the key is computed by:

Not the most advanced protection, as you can see. That said, as to not lay out everything in this post, finding the salt prefix is left as an exercise to the reader; it’s not too difficult. :)

If you’re following along and want to check your work, the key for index.html is:


and the SHA-1 sums of the encrypted and decrypted files are:

$ shasum index.html index.html.dec
3c49b8aa65bd24c376485bd6cf9da238367515c7  index.html
52c6ddf10ce91c84ed38e5be80e4377d58b2b90c  index.html.dec

All of this research was done on BIAS FX for macOS.

Debugging (without LLDB)

Since the UI is all web-based anyway, native debuggers (e.g. LLDB) are unlikely to be of much help here for debugging the UI. However, a very important detail was mentioned earlier: on macOS, the UI uses a native WebKit web view. This means that like other native web views running on the system, it can be debugged using Safari’s developer tools.

BIAS FX's UI in Safari's Web Inspector

Simply opening the “Develop” menu in Safari and looking in the submenu matching your computer’s hostname will reveal all of the debuggable web views on the system. If a BIAS FX plugin window is open, it will show up in the list.

Wrapping up

This was a fun project, but wasn’t really started with any goal in particular and as such, has no logical conclusion. There’s probably a lot more to be explored with the web inspector, but I ran out of time to experiment further.

To conclude, I have a handful of final notes that didn’t really fit in anywhere else in this writeup, but might be relevant for any future research:

As always, feel free to reach out if you have any questions.