0:00
This audio is presented by Hacker Nune, where anyone can learn anything about any technology.
0:05
I hit a watermark in screenshots, and iOS thought it was a password, by Tamo Runky.
0:10
In 1996, Hotmail added, PS, I love you.
0:15
Get your free email at Hotmail, to every outbound email, 18 months later, 12 million users,
0:21
400 million dollars exit to Microsoft. Apple pulled the same trick a decade later with
0:27
sent from my iPhone. Every user was running an ad campaign without knowing it.
0:32
Neither company paid for a single impression. Last month, I tried the same thing on an app I work on.
0:38
It worked. It also made iOS think the users were typing passwords.
0:42
Why bother? The app is an ever-evolving interactive story, an ongoing narrative that responds to
0:48
the reader, with characters who remember and react as it unfolds. The messages between the reader
0:53
and the characters appear in a chat-like view, and people screenshot funny or weird moments and
0:58
post them to discord servers, TikTok, and group chats. Attribution says screenshots are the
1:03
single biggest source of signups. Bigger than any ad channel, bigger than any influencer campaign.
1:09
Once that shows up in a dashboard, one question gets loud, what is on those screenshots?
1:14
Because whatever's on them is the ad, and until recently, the screenshots went out with no branding
1:20
at all. The obvious move is a logo in the corner. TikTok, does it? The problem with a logo is that it
1:26
reads as corporate. It makes the screenshot feel like an ad, and people with any taste-crop it out
1:31
are switched to a competitor that doesn't stamp its content on theirs. I wanted a mark that
1:36
belonged to the aesthetic, not something glued on top of it. The why, and idea. If you've spent
1:42
time near fanfic, you know why, and it stands for your name, the reader insert placeholder
1:46
writers drop into stories so readers can imagine themselves in the scene. It's also conveniently the
1:52
name of the app, so the watermark wrote itself, a tiny colored badge next to every character's name
1:57
in the chat, reading why, and in the right corners of the internet, it reads Asa Native in joke.
2:03
In the wrong ones, it reads as a mystery someone has to ask about. That was the mechanic.
2:09
Screenshot goes out, a friend sees the badge, the yask, wait, what app is this? But I didn't want
2:15
why, and visible while people were using the app. That would just be another logo in the corner.
2:20
I wanted it to show up only when someone takes a screenshot.
2:24
The reveal on screenshot trick, there's a quirk of iOS that most developers only bump into when
2:29
dealing with passwords. A with doesn't just mask text with dots, the entire rendering canvas for
2:35
that field is stripped from screenshots and screen recordings. It's how iOS keeps your
2:40
password off other people's screens during airplay and screen sharing. I used it for something
2:45
Apple didn't intend. The component creates an invisible, non-interactive, flips it to secure,
2:50
and reaches into its private subview hierarchy to find a class called. That's the internal UI
2:56
kit view that owns the screenshot hidden rendering. Inside that canvas, I inject a colored cover.
3:01
Underneath the whole stack, rendered normally, sits the y and badge. In the app, the user sees the
3:08
colored cover sitting next to the character. The badge is occluded. Everything looks clean.
3:13
The moment someone takes a screenshot, iOS blanks out anything inside the secure canvas.
3:18
The cover vanishes. The y and underneath becomes visible. The screenshot is the ad. I ship this
3:24
on a Thursday. I spent the next hour taking screenshots of my own chats just to watch the y and
3:30
appear. On Monday, the support ticket started. iOS thinks you're typing a password.
3:35
Users wrote in about the chat input acting strangely. Auto-correct would sometimes stop.
3:40
The keyboard would occasionally offer a strong password, suggestion bar. On iOS 17 plus,
3:47
it would offer to save what they'd type to keychain as a password. The chat input is obviously
3:52
not a password field. It had, basically every incantation react native and iOS exposes for
3:58
please leave this input alone. None of it stopped the behavior. My first theory was react native.
4:04
The chat input is inside a scroll view. Andron's iOS text handling has a long history of leaking
4:09
state between fields. It spent a day pulling the input out of the chat screen, mounting it in
4:14
isolation, and logging every prop iOS might read. It behaved perfectly in isolation. Back on the
4:21
chat screen, the strong password bar reappeared. My second theory was a third-party keyboard extension
4:27
that one of the users had installed. It wasn't. What finally gave it away was a bug report with
4:32
a screen recording. The user had scrolled up, the chat was empty above the fold, and the keyboard was
4:38
behaving normally. Scroll down into the populated chat, and the strong password bar appeared.
4:44
The input was the same input. The only thing that changed was what was on screen around it.
4:49
The input misbehaved only after the chat was populated, and only with character messages,
4:54
the ones that rendered a watermark. Empty chats and user only messages were fine. Every watermark
5:00
view drops an invisible within the view tree. A busy chat screen has 20 or 30 of these.
5:05
iOS's keyboard heuristics look at the surrounding view hierarchy when they decide how to treat
5:10
the focused input. The keyboard sees a screen littered with secured text fields and reasonably
5:15
concludes that the user is in a password flow. I had turned every watermark into a little lie that
5:20
iOS believed. The feature I was exploiting to hide the watermark was polluting the signal iOS uses
5:27
to classify live text inputs. The fix, the password heuristic is triggered by objects in the view
5:33
hierarchy. If I could get the same screenshot hiding behavior without putting any S in the tree,
5:38
the heuristic would stop firing. The screenshot hiding that triggers is actually a flag on the
5:43
underlying. It shouldn't document it but the property exists, set the right bits, and the layer
5:49
disappears from screenshots and screen recordings. Is one of the pad's use exposes that internally flips
5:54
this flag, setting it directly on a layer you control does the same thing, with less machinery.
6:00
The new implementation swapped the incandescalunking for a single with said via reflection.
6:06
Same screenshot hiding behavior, no in the tree, no keyboard heuristic firing on the chat input.
6:12
Shipping this meant accepting some ugliness is a private, undocumented API. Apple prohibits private
6:18
API calls under App Store review guidelines. Any iOS release can remove the property without warning
6:24
and App Store review scan submitted binaries for known private API names.
6:28
That last one is why the property has to be accessed via runtime reflection on a base 64 encoded
6:34
string. It keeps the plaintext name out of the shipped binary. None of this was new territory.
6:40
Wasn't a public class either. I was trading one private API hack for a cleaner one.
6:45
What change? The watermark is still running. Support tickets about broken auto correct stop once
6:51
the fix went out. The next app store reviewed took the build without complaint.
6:55
Screenshots going out from the app now carry a small y and stamp next to character names,
7:00
visible only in screenshots. I can't cleanly attribute the sign up lift to the watermark
7:05
because screenshots were already the top channel and the watermark is subtle by design.
7:10
What I care about more is that it exists. The next friend of a user who sees a screenshot
7:15
in a discord server and wonders what app it's from now has a concrete thing to ask about.
7:20
Two things worth knowing up front. If you're building in this territory, two things I wish I'd
7:25
known on day one. First, iOS has more heuristics than you can easily enumerate.
7:30
Password detection is one, keyboard behavior, autofill, haptic classification,
7:35
and many UI kit features look at the surrounding view hierarchy rather than just the focused view.
7:40
If you're borrowing OS features for uses the OS never imagined, expect unrelated things to break
7:46
in places you didn't touch. Second, the private API line is not a line you cross only once.
7:52
Every iOS update has a tiny role of the dice on whether the thing you built still works,
7:57
and whether Apple has noticed. I have a note pinned in notion titled,
8:01
read these release notes first when iOS N plus one drops with a list of framework names.
8:07
That note will pay for itself at some point, probably at 2 a.m. on a Tuesday in September.
8:13
If you've shipped something similar, Android equivalents especially welcome,
8:16
NYDMs are open. Thank you for listening to this Hackernoons story,
8:21
read by artificial intelligence. Visit hackernoons.com to read, write, learn and publish.