This is a direct followup to to Getting 32 bit API Response Data in CSS
In CSS, 16 bits
of response data, placed both in the intrinsic width and in the intrinsic height, was a huge improvement from not being able to get API response data at all (without JavaScript)...
However, in the days after I figured it out, my mind leaned in one direction:
It would be way more fun if it was 16 bits
32 times instead of just twice.
Packing 512 bits into an SVG for Exfiltration
I was meditating before bed and got struck with another inspired thought -
"What if it was possible for the image document itself to animate its own intrinsic size?"
The usual chain of realizations after an inspired thought each flashed with understanding on how it could be accomplished... I just needed to figure out if such an image type existed.
I grabbed my phone and and searched across all the animated image formats I knew of, seeing if any of them were capable of it. svg, webp, apng, gif, maybe weird video formats? I couldn't find any.
In the morning, something in me said to keep digging at SVG.
I tried embedded CSS, media queries, use'd defs, more use'd defs, diving into the numerous SVG animate docs, trying to trick them, and read up on other ideas and animation related properties - nothing could let me set height or width.
But that last link made me think...
...what about viewBox
? I'd have some other hurdles to tackle but... Is that possible to animate?
vvv
IT IS!!
^^^
Sorting out the solution space
Now the problem is, if you don't set width
and height
attributes on the root svg
element then try to use the svg as content
on a pseudo
, it renders 0px
x 0px
because it's a vector document and no longer has an intrinsic size.
So I searched and added preserveAspectRatio to it... Still 0x0
... but then in my CSS, I pinned the width to 10px
and let the preserved aspect ratio of the viewBox
determine the height (which I can change with an animation embedded in the SVG) aaand... the html element containing it grew to the expected height.
:3
If there was only one frame, this took my original 32 bits and cut it in half; since only one dimension can be exfiltrated while the other is static.
BUT! Now, my 2nd dimension was time and the first is at the mercy of time, so there's more than enough data to be had.
How exciting!
I learned all I could about how to control the animation in SVGs and whipped up a server side script to generate my first animated SVG:
<?php
header('Content-type: image/svg+xml');
$data = array(
400,
450,
150,
20,
175
);
$datalen = count($data);
$viewBoxXYWidth = '0 0 10 ';
$frames = array_map(function ($val, $index) use ($viewBoxXYWidth) {
return $viewBoxXYWidth . ((string) ($val));
}, $data, range(1, $datalen));
$dur = $datalen * 0.33; // total seconds
$keytimeStep = 1 / ($datalen); // uniform portion per frame
$keytimes = implode("; ", array_map(function ($index) use ($keytimeStep) {
return ($index * $keytimeStep);
}, range(0, $datalen - 1)));
$values = implode("; ", $frames);
echo '<svg viewBox="0 0 10 100" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg">
<animate
attributeName="viewBox"
dur="' . $dur . 's"
fill="freeze"
begin="0.1s;"
values="' . $values . '"
keytimes="' . $keytimes . '"
repeatCount="indefinite"
calcMode="discrete"
/>
</svg>';
?>
(why php?! - because I already had a server I've been paying for for years, set up to run php out the gate.... And even though I've earned a wonderful living knowing JavaScript and node really well, sometimes it's fun to look up every single function, operator, and syntax to progress through something you know you can do without knowing specifics. lol)
Now let's fork my first CodePen from the previous article to see CSS responding to and changing size --vars as the SVG ticks along:
Confirmed! We can read the size change. Like in the previous article, in the end, it'll be using the view-timeline
measurement technique instead this tan(atan2()) technique.
It cooks my cpu so we'll want to remove it from content
when the exfiltration completes.
Conceptually, how to procedurally exfiltrate 1D + time
The demo above isn't very useful on its own. It reports a copy of the height whenever it's there but we need to save it... and what good is a bunch of 16 bit values if you don't know and can't trust the order?
I know I can accumulate values in CSS with the CPU Hack and mathematically determine which --var gets incoming values updated instead of just holding its previous value, so I won't worry about CSS specifically. How, in general, could we exfiltrate 1D over time?
Sentinel values to the rescue!
The size of the measuring area we use doesn't HAVE to be limited to 16 bits even if I want to limit the data packets themselves to 16 bits. So we can pack some sentinels in there too. css-api-fetch ships with the ability to handle values up to 99999
, which is well above 65535
(the 16 bit ceiling).
So what do we need to know?
What problems might we run into?
If two values in our data are the same back-to-back, we need an interruption to know it's two distinct packets. I already decided we'll be aiming for 512 bits, so we need the SVG's animation to have a maximum of 32 16-bit data frames, with sentinel frames in between...
If the CPU is feeling heavy, the SVG animation may appear to skip discrete steps entirely. That means we need some way of always knowing what step we're on. So rather than a single "between data frames" sentinel, let's use the data index (1 based like CSS nth-* selectors) as the sentinel value, making it its own discrete step before the discrete step showing the data for that index.
Sentinel index -> data -> sentinel index -> data ...
That lets us know when it loops too, potentially when we hit sentinel 1
.
But how do we know that it didn't skip to a different data frame and accidentally have us record it in the wrong slot?
We need to let it loop and keep going until it's right, and the best way to know if data is correct is a checksum! So we need another data frame, and a sentinel for that value.
Creating the Checksum Algorithm
I could use css-bin-bits
to XOR all the data, but it's quite heavy and isn't needed anywhere else - let's settle on an alternative that's simple to do in CSS.
Mathematically, if you take a 16 bit value, divide it by 256 (floor to integer), and take the 16 bit value again modulo by 256, you get the high and low bytes. Add those 8 bit values together and you're at 9 bits. This feels like a reasonable checksum approach, let's circle back to this though.
We don't have to stay in 16 bit range to compute the checksum as long as the final checksum is 16 bits, so let's just sum all (up-to) 32 values.
We've got to be careful about incorrect storage writes due to skipped frames though, so let's add the even index values twice so there's some semblance of order.
That sum, 16 bit values
, 32 times
, plus an additional 16 times
, is about 22 bits
. Divide and module 11 bits
on each side circling back to the earlier thought, then add those together, giving 12 bits
as our checksum response.
Seems reasonable... It's not completely error proof but the SVG would have to be skipping several steps to mess it up in a way to MAYBE generate the same checksum now... In any case, let's also send back the data length
and include that in the checksum as well, just by adding it as the last step of our checksum. The max data length (number of 16 bit values we want to handle) is only 32
, so adding the length value to the 12 bits doesn't come anywhere near pushing us over 16 bits. Yay!
spoiler: this is what I did but CSS became lossy somewhere around 21 bits so I split it up and effectively did the same algorithm but in smaller chunks at a time. Server side uses the alg exactly as described.
Technically with the setup we've described, it doesn't matter what the order is in the animation as long as each sentinel tells you what index the next frame is supposed to be in the data.
One more thing, let's put the data length value
first in the response and add a sentinel for that too (sentinel in the SVG animation before the value, as with the rest of the data).
That's 34 sentinels. The SVG viewBox
height can't be 0
and CSS will benefit from allowing 0
to represent no data internally, so let's say we have 35 sentinels with 0
deliberately unused.
All data frames now get embedded in the SVG with 35
added to their value. Length
and checksum
data values ALSO get 35
added to the viewbox value. viewBox
heights in the SVG Animation representing the sentinels will have values 0 to 34 (skipping 0) and each tell us exactly what the next frame in the SVG Animation represents.
CSS side, we just check if the raw measurement is greater than 34, it's data
so subtract 35
from it, if it's less than 35
, it's a sentinel
.
Beginning to Exfiltrate the 512 bits with CSS
After I finished the PHP side to generate the SVG animation as detailed, I thought on specific ways to begin the CSS for this exfiltration process.
Here's the PHP code!
<?php
header('Content-type: image/svg+xml');
$data = array(
400,
450,
150,
20,
175
);
$datalen = count($data);
$viewBoxXYWidth = '0 0 10 ';
// add 35 to all the values so we can use 0 to 34 for sentinels. 0 = CSS-side sentinel, 1-32 = data frames, 33 = length, 34 = checksum
$frames = array_map(function ($val, $index) use ($viewBoxXYWidth) {
return ($viewBoxXYWidth . ((string) $index) . '; ' . $viewBoxXYWidth . ((string) ($val + 35)));
}, $data, range(1, $datalen)); // 1 up to 32 = indicator that next frame is the value(+35) for that index(1-based)
// no matter how many are in the array, '33' indicates the next frame is data length, which is used in the checksum too
array_unshift($frames, $viewBoxXYWidth . '33; ' . $viewBoxXYWidth . ((string) ($datalen + 35))); // + 35 b/c data
// unshift so the length is (hopefully) the first value read and a sense of progress can be reported
$fullsum = 0;
for ($x = 0; $x <= ($datalen - 1); $x++) {
// double the odd ones so there's some semblance of order accounted for
// the odd ones with 0 based index is the even ones on the CSS side
$fullsum += ($data[$x] + (($x & 1) * $data[$x]));
}
$checksum = floor($fullsum / 2048) + ($fullsum % 2048) + $datalen + 35; // + 35 because it's data
// no matter how many are in the array, '34' indicates the next frame is checksum
array_push($frames, $viewBoxXYWidth . '34; ' . $viewBoxXYWidth . $checksum);
$actualNumItems = count($frames) * 2;
$dur = $actualNumItems * 0.33; // total seconds
$keytimeStep = 1 / ($actualNumItems); // uniform portion per frame
$keytimes = implode("; ", array_map(function ($index) use ($keytimeStep) {
return ($index * $keytimeStep);
}, range(0, $actualNumItems - 1)));
$values = implode("; ", $frames);
echo '<svg viewBox="0 0 10 100" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg">
<animate
attributeName="viewBox"
dur="' . $dur . 's"
fill="freeze"
begin="0.1s;"
values="' . $values . '"
keytimes="' . $keytimes . '"
repeatCount="indefinite"
calcMode="discrete"
/>
</svg>';
?>
There are a few ways to accomplish this in CSS and potentially many more coming with recent spec additions.
My first approach is the easiest conceptually - using a view-timeline for every piece of data and doing the same thing over and over. It was working but I groaned through my progress in displeasure with how gross it was. That'll be nearly 40 animations on :root
if I continued.
So I went to sleep.
When I woke up, I laid there several moments looking out the window smiling with that just-woke-up-or-meditated buzzy feeling, then a firehose of thoughts rushed into my head. I rolled over, grabbed my notebook and the closest pen, sat up in bed, and started writing down the algorithm to exfiltrate it with just 6 CSS animations.
Literally solved on paper; This is EXACTLY how it's implemented.
I got up, got on my computer, ignored my previous work, and opened a new CodePen.
I set up the 4 html elements indicated among the chicken scratch there, then flooded the CSS panel with notes around 4 empty class selectors corresponding to them. It won't be on :root
now but we can place anything that relies on it inside.
Not a single piece of functi
Author Of article : Jane Ori Read full article