Version Zero Easter Eggs

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.

Google Docs’ insert character dialog

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
## 
## ──────────────────────────────────────────────────────────────────────────────



See also