I’ve just finished reading ‘Version Zero’ by David Yoon. I really enjoyed it. There’s some (javascript) code on some separator pages between some of the chapters that is loosely tied into the plot and general theme of the book. I love solving puzzles, so what was I supposed to do, just leave it at that?
Incidentally, I’ve hooked my reading list into my mini blog so my ‘Currently Reading’ list is (ideally) up to date.
I can’t help myself when it comes to puzzles or easter eggs like this. Decrypting the new Australian 50c coin puzzle triggered a conversation with one of our top spy agencies. I learned a whole lot from solving this Gaussian Primes puzzle.
I very much enjoyed the book. I won’t give away too much, but it does a great job of calmly building up a story, the characters, the plot and then ramping up the excitement. In the acknowledgements the author thanks his genius nephew Eric Yoon (yoonicode.com - what a great URL!) for the easter egg code.
I figured I’d try to to run the code and see what it does. I wanted to carefully read this - especially given the theme of the book - to make sure it wasn’t going to delete my networking setup or something. The first step was to get the code from the (paper) pages into a computer. I looked around - web search, GitHub, Eric’s site - and couldn’t find an online copy anywhere. I tried searching for a few other unique-looking terms in the code but nothing. Has no one written up a discussion about this easter egg? The book is from 2021, so it’s not that new. I came across it randomly walking the shelves at my local library (credit to librarians for prominently featuring great suggestions!). Maybe I’ve just overlooked a write up somewhere, but maybe I’m the first?
I had a go at OCR via tesseract but since it’s javascript and not a text language, it didn’t have much luck. There are supposedly some language packs for tesseract but none of them helped with the images I have.
So, with no digital copy of the code to pull in, I guess the only thing to do is
to forget about it and move onMANUALLY TYPE ALL 80-SOMETHING LINES IN. This
was … interesting, but not too bad, really. The choice of font in the book and
somewhat
low dpi meant that the difference between (
and {
was very subtle. Having a
little bit of domain knowledge helps in this case. Entering the code was otherwise just a matter
of typing until I got to this (or what looks something like this) line
subunit.push(btoa("ß]xëÏz×|ç¼Û¾v"));
Oh… No.
My best guess was to use Google Docs’ ‘insert character’ which lets you draw the symbol you’re looking for and gives some options.
The next step was to actually confirm that I hadn’t
made any transcription errors, which should show up if I try to run the code. I checked
that I could run a file of javascript code with node
and it worked, sort of. It failed about
halfway through because a function wasn’t defined - btoa
and atob
are deprecated in Node. I added some definitions
for atob
and btoa
and that was resolved
atob = a => Buffer.from(a, 'base64').toString('binary')
btoa = b => Buffer.from(b, 'binary').toString('base64')
The code didn’t run, though, because some values didn’t convert to BigInt
,
probably because I’d entered them wrong.
I carefully went through what I’d entered and did realise that there was only a subtle
difference between 1
(one) and l
(lowercase L
) and made some fixes.
Apart from that, the code ran until it hit the BigInt
conversion of a
particular value and failed. I forced the code to skip over that value and got to
an answer. The result of running the code is similar to the Advent of Code 2022
Day 10 Part Two
puzzle which writes out #
and spaces in lines to
spell out ASCII-art words. Clearly my code was broken, because I could see what it
was supposed to spell out, and there were errors.
I figured I had to trace back through the result being built up and figure out
which values end up on which lines. Extra fun, because there’s a filter
in the
middle that removes some of the input. Sure enough, one of the offending lines was
the unicode-salad line above - great. The other seemed to be
const datetime = new Date(1997, 7, 24);
Super great - this is going to involve a timezone issue, isn’t it?
Back to the unicode, I searched again for this specific line (minus the unicode) and actually did get a hit - Google Books has a copy of the (Czech?) translation of the book and returns this line. Not precisely (something hasn’t encoded correctly) and not selectable in the book, but selectable in the Google result for it. That wasn’t much help after all.
Let’s walk through the code and see how it works before we resolve it. A full copy (with my own annotations and fixes) is here, if anyone else wants to not have to type all of it in.
const VERSION_NUMBER = 0;
const AGENT = "BLACK HALO";
const year = 0x2018;
const enc = [
021, 024, 015, 015,
026, -031, 030, 016,
034, 027, 021, 034,
021, 014, 025, -022,
017, 016, 032, 027
];
let res = ["You are infinite"];
RANDOM_SEED = 20879976793454946324n;
## 0
## BLACK HALO
## 8216
## 17,20,13,13,22,-25,24,14,28,23,17,28,17,12,21,-18,15,14,26,23
## You are infinite
## 20879976793454946324
So far so good. Some constants and the start of a result res
- an array
containing some text.
if (VERSION_NUMBER % 2 < 1) res.shift();
With VERSION_NUMBER == 0
this just drops the first (and only) value of res
, so
we’re back to an empty result.
res.push(enc.map((i, idx) => {
return String.fromCharCode(
AGENT.charCodeAt(
idx % AGENT.length
) - i
);
}).reduce((i, j) => {
return i.toString() + j.toString();
}));
res[0]
## 18465903081007629328
This does some math on the characters of AGENT
and produces what will eventually
be the second line of actual output (currently the first).
res.unshift(atob("MzU3NzU1MDM2NTgxMDMzNTg0OTU="));
res[0]
## 35775503658103358495
This becomes the first line of output due to the unshift
.
res.push(
(8939935261623587079n << 2n).toString()
);
res.push((RANDOM_SEED & 0x18C445CAC40447832n | 0n).toString());
res.push("" + (151845383424178857009896n / BigInt(year)));
## 35759741046494348316
## 18465906380616247312
## 18481667894861107231
become the third, fourth, and fifth line of the output.
The next lines set up something to be used later,
let as_json = {
coordinates: '{"x": 2, "y": 5}',
tolerance: 0.1,
subunit: [2 ** 8]
};
const c = JSON.parse(as_json.coordinates);
## { coordinates: '{"x": 2, "y": 5}', tolerance: 0.1, subunit: [ 256 ] }
## { x: 2, y: 5 }
Then adds a separator of 0
to the result
res.push((z => `value: ${z}`.slice(7))((x => x >>> 42)(3 ** 5)));
## 0
The next part is a bit of a red herring since it sets up a subunit
object
let subunit = as_json["subunit"];
eval("subunit" + `${String.fromCharCode(46)}pop()`);
but the eval
results in
## subunit.pop()
so that is back to empty.
This adds some data to subunit
subunit.push(69 + 114 + 105 + 99 + 32 + 89 + 111 + 111 + 110);
## 840
but again it’s overwritten with
subunit[0] = Math.round(euclidianDistance(c.x, c.y, 48, 1967.46095)) + "4568824394612736";
[...]
/**
* @returns the distance between 2d point (x, y) and (x1, y1)
*/
function euclidianDistance(x, y, x1, y1) {
return Math.sqrt(((x - x1) ** 2) + ((y - y1) ** 2));
}
## 19634568824394612736
This is the first line of the second block of output.
The next line of output should come from the code that I have as
subunit.push(btoa("ß]xëÏz×|ç¼Û¾v"));
res = res.concat(subunit);
but that produces
## 3114689613znvNu+dg==
which doesn’t convert to BigInt
at all. We’ll come back to that.
const str = "MjQyNDI4NzczNDQ0MjgwNjQ3Njg=bMTk2MTc2ODAxMTY0MTIzMTc2OTY=bMTk2MzQ1Njg0OTI2MDgzODkxMjA=bMA==";
res = res.concat(str.split("b").map(b => atob(b)));
splits up the str
at the letter b
and runs atob
over the pieces
## [
## '24242877344428064768',
## '19617680116412317696',
## '19634568492608389120',
## '0'
## ]
providing the rest of the second block of output and the next separator.
The next lines set up a Date
object and extracts part of the string representation
(local timezone, but it’s just taking the "19"
from "1997"
)
const datetime = new Date(1997, 7, 24);
res.push(
datetime.toString().slice(11, 13) +
(
634601705079659136n +
BigInt(datetime.getTime())
)
);
## 19634602577426259136
which looks okay, but gives the wrong value on the first line of the last block - another one to come back to.
The next lines were fun to enter and validate (not)
res = res.concat(
[
"Mjg4NDIxOTU1MjI5NzAyMDYyMDg=", /** block 3, line 2 */
"MTVkIGhlcnJpbmcgZ2V0IHJla3Q=",
"MTEwNTI5MDA1Mjk2MDU5NzY2MTM0",
"MjQyMzA1MjI2OTg2ODIzNjM5MDQ=", /** block 3, line 3 */
"SG9wZSB5b3UgbGlrZSBSZWdleCE=",
"MTk1MjA0NjkyMDUyODYzMDQ0MDM=",
"MjE5MjQ2NjY0OTUzMjkxMjQzNTI=", /** block 3, line 4 */
"MjYwMjg2MDQ4NjAyODMwNTUxMDI=",
"MTk2MzQ2MDI1OTM2MDc1MTUxMzY=", /** block 3, line 5 */
"TG92ZSwgUGlsb3QuIDwzICA8MyA=",
"MzA0NTgyNTg0Mzk1NzM4OTU3OTM5"
].filter(
i => i.match(/M[j|T].+[QUINOA][x12][DjTLMNOP]{2}[^aeiou]\*?.{1,5}[a-zA-Z5]+=/g)
).map(atob)
).map((i, j) => {
if(j > 5 && j < 12 && j != 7) {
return BigInt(i) & BigInt(31775n << 50n);
}
return i;
});
This involves filtering some entries from the big block of encoded text,
running the remaining ones through atob
, then doing some math on these combined
with all the other values from res
(effectively only updating the second block of values).
While debugging this, I found another easter egg hidden within - one that wouldn’t be found just by running the code itself. Some of the lines filtered out by the regex convert to plaintext!
atob("MTVkIGhlcnJpbmcgZ2V0IHJla3Q=")
atob("SG9wZSB5b3UgbGlrZSBSZWdleCE=")
atob("TG92ZSwgUGlsb3QuIDwzICA8MyA=")
## 15d herring get rekt
## Hope you like Regex!
## Love, Pilot. <3 <3
Niiiiice!
The final lines of code take these values, convert to binary, and print a #
for
each 1
(and a space otherwise)
for (const i of res) {
const bin = BigInt(i).toString(2);
let ln = "";
for (const j of bin) ln += j == "0" ? " " : "#";
console.log(ln);
}
If you do that, a message (slightly corrupted) appears.
I decided to work backwards, since I was fairly sure what the ‘right’ solution should be. Taking those lines (manually corrected), converting them all the way back through the processing in reverse, I could see what the ‘right’ code should be.
The unicode line that produces what I think is the “right” solution is
subunit.push(btoa("ß]xëÏyÓm¼÷\x8DùÓ}ú"));
The characters are mainly close but not perfect, so maybe a LOCALE
issue? Something
to do with Linux (which I’m on) vs Windows?
The date line seems to be off by exactly 16 hours and 30 minutes which is
disturbingly likely to be a timezone issue. I’m at GMT+10:30 (Adelaide, South
Australia) at the moment. StackOverflow seems to have a lot of angry
comments
regarding whether or not this is an issue for Date()
. I seem to be able to get
the “right” solution with
const datetime = new Date(1997, 7, 24, 16, 30);
With all that in place, it’s time to run all of the code! If I do that, I get…
« Click to reveal! »
##### ##### ##### ##### ##### # # #####
# # # # # # # # # #
##### # # ##### # ## # # # ###
# # # # # # # # # # #
# ##### # ## ##### ##### # #####
# # #####
## ## #
# # # ###
# # #
# # #####
# # ##### ##### # # #####
## # # # # # # #
# # # # # ### # # ###
# ## # # # # # #
# # ##### ##### ##### ##### #####
which is fitting, and thoroughly satisfying to finally produce.
I also ran this code (with my corrections, minus the atob
and btoa
definitions) over on jsfiddle.net and it seems to give
the right solution, which makes me think perhaps it really is an error in the
code or how it was printed.
What an adventure! I learned a lot of javascript (how to run it with node
and
in a browser for debugging), played with
tesseract, and learned about
entering unicode. I’m sending this to Eric Yoon for comment and will update if I
hear anything.
As a side note for this post, you’ll notice that the code blocks are all nicely
rendered as usual - in this case they’re the actual javascript from the easter
egg code. {knitr} does have a way to evaluate
javascript in code chunks with the node
engine, but that essentially runs
node -e 'CODE'
on each chunk independently, so you can’t define a variable in
one chunk then reference it in another. That wasn’t sufficient for this
exploration. I did find an (old)
implementation that uses
{V8} in Yihui’s (already experimental)
{runr}, but it was written for a much older
version of {knitr} and was out of date.
So, of course the thing to do was just hardcode the outputSHAVE A YAK AND UPDATE
THE IMPLEMENTATION. If you’d like to have javascript code chunks in your Rmd, I’ve made
a pull request to that original implementation
and have my own fork.
It seems to work okay, with the exception that it doesn’t pull in Buffer
so my
custom atob
function doesn’t work, and it doesn’t have another. It’s also going
wrong in terms of the persistent context in that the const
and let
directives
are being seen multiple times and it doesn’t like that. Otherwise, variables persist
across chunks just fine - these chunks are fully live:
Define a variable x
x = 1 + 5;
## 6
Then continue the block
x + 12
## 18
So, that’s working.
As always, leave a comment if you have one, or find me on Mastodon (I’m much less on Twitter these days). If you have a correction or annotation to add to the code it’s here.
devtools::session_info()
## ─ Session info ───────────────────────────────────────────────────────────────
## setting value
## version R version 4.1.2 (2021-11-01)
## os Pop!_OS 22.04 LTS
## system x86_64, linux-gnu
## ui X11
## language (EN)
## collate en_AU.UTF-8
## ctype en_AU.UTF-8
## tz Australia/Adelaide
## date 2023-03-31
## pandoc 2.19.2 @ /usr/lib/rstudio/resources/app/bin/quarto/bin/tools/ (via rmarkdown)
##
## ─ Packages ───────────────────────────────────────────────────────────────────
## package * version date (UTC) lib source
## blogdown 1.13 2022-09-24 [1] CRAN (R 4.1.2)
## bookdown 0.29 2022-09-12 [1] CRAN (R 4.1.2)
## bslib 0.4.1 2022-11-02 [3] CRAN (R 4.2.2)
## cachem 1.0.6 2021-08-19 [3] CRAN (R 4.2.0)
## callr 3.7.3 2022-11-02 [3] CRAN (R 4.2.2)
## cli 3.4.1 2022-09-23 [3] CRAN (R 4.2.1)
## crayon 1.5.2 2022-09-29 [3] CRAN (R 4.2.1)
## curl 4.3.3 2022-10-06 [3] CRAN (R 4.2.1)
## devtools 2.4.5 2022-10-11 [1] CRAN (R 4.1.2)
## digest 0.6.30 2022-10-18 [3] CRAN (R 4.2.1)
## ellipsis 0.3.2 2021-04-29 [3] CRAN (R 4.1.1)
## evaluate 0.18 2022-11-07 [3] CRAN (R 4.2.2)
## fastmap 1.1.0 2021-01-25 [3] CRAN (R 4.2.0)
## fs 1.5.2 2021-12-08 [3] CRAN (R 4.1.2)
## glue 1.6.2 2022-02-24 [3] CRAN (R 4.2.0)
## htmltools 0.5.3 2022-07-18 [3] CRAN (R 4.2.1)
## htmlwidgets 1.5.4 2021-09-08 [1] CRAN (R 4.1.2)
## httpuv 1.6.6 2022-09-08 [1] CRAN (R 4.1.2)
## jquerylib 0.1.4 2021-04-26 [3] CRAN (R 4.1.2)
## jsonlite 1.8.3 2022-10-21 [3] CRAN (R 4.2.1)
## knitr 1.40 2022-08-24 [3] CRAN (R 4.2.1)
## later 1.3.0 2021-08-18 [1] CRAN (R 4.1.2)
## lifecycle 1.0.3 2022-10-07 [3] CRAN (R 4.2.1)
## magrittr 2.0.3 2022-03-30 [3] CRAN (R 4.2.0)
## memoise 2.0.1 2021-11-26 [3] CRAN (R 4.2.0)
## mime 0.12 2021-09-28 [3] CRAN (R 4.2.0)
## miniUI 0.1.1.1 2018-05-18 [1] CRAN (R 4.1.2)
## pkgbuild 1.3.1 2021-12-20 [1] CRAN (R 4.1.2)
## pkgload 1.3.0 2022-06-27 [1] CRAN (R 4.1.2)
## prettyunits 1.1.1 2020-01-24 [3] CRAN (R 4.0.1)
## processx 3.8.0 2022-10-26 [3] CRAN (R 4.2.1)
## profvis 0.3.7 2020-11-02 [1] CRAN (R 4.1.2)
## promises 1.2.0.1 2021-02-11 [1] CRAN (R 4.1.2)
## ps 1.7.2 2022-10-26 [3] CRAN (R 4.2.2)
## purrr 1.0.1 2023-01-10 [1] CRAN (R 4.1.2)
## R6 2.5.1 2021-08-19 [3] CRAN (R 4.2.0)
## Rcpp 1.0.9 2022-07-08 [1] CRAN (R 4.1.2)
## remotes 2.4.2 2021-11-30 [1] CRAN (R 4.1.2)
## rlang 1.0.6 2022-09-24 [1] CRAN (R 4.1.2)
## rmarkdown 2.18 2022-11-09 [3] CRAN (R 4.2.2)
## rstudioapi 0.14 2022-08-22 [3] CRAN (R 4.2.1)
## runr 0.0.7 2023-03-31 [1] local
## sass 0.4.2 2022-07-16 [3] CRAN (R 4.2.1)
## sessioninfo 1.2.2 2021-12-06 [1] CRAN (R 4.1.2)
## shiny 1.7.2 2022-07-19 [1] CRAN (R 4.1.2)
## stringi 1.7.8 2022-07-11 [3] CRAN (R 4.2.1)
## stringr 1.5.0 2022-12-02 [1] CRAN (R 4.1.2)
## urlchecker 1.0.1 2021-11-30 [1] CRAN (R 4.1.2)
## usethis 2.1.6 2022-05-25 [1] CRAN (R 4.1.2)
## V8 * 4.2.2 2022-11-03 [1] CRAN (R 4.1.2)
## vctrs 0.5.2 2023-01-23 [1] CRAN (R 4.1.2)
## xfun 0.34 2022-10-18 [3] CRAN (R 4.2.1)
## xtable 1.8-4 2019-04-21 [1] CRAN (R 4.1.2)
## yaml 2.3.6 2022-10-18 [3] CRAN (R 4.2.1)
##
## [1] /home/jono/R/x86_64-pc-linux-gnu-library/4.1
## [2] /usr/local/lib/R/site-library
## [3] /usr/lib/R/site-library
## [4] /usr/lib/R/library
##
## ──────────────────────────────────────────────────────────────────────────────