The mask digit
The "version" digit of every GUID in A858's lists is C#'s Random seeded with the current millisecond.
Most of A858's posts, both the plain lists from July 2011 and the inner layer of the 2012–2013 encrypted posts,
are lists of GUIDs in a slightly odd format. A858 itself hinted at this format in the
PostAnalyzer code (posted as hex in
ihicx):
if (raw.Substring(13, 1) == "4")
return root.DecryptRaw(raw);
else
return root.DeMD5(raw);
Finding: the odd digit is the output of the standard .NET System.Random, seeded with a number from 0 to 999
(the current millisecond). This explains every masked GUID list in the corpus.
The other 6 are lists of 32-bit words rather than GUIDs (ihwxg, iiafz, iirqk, jqt3x, mymne, n1ley; see the 32-bit word posts), so their seed drives Next(lo, hi) calls, not a mask digit.
The format
A GUID is 16 bytes, usually written as 32 hex characters. The first character of the third group is the version, and the first character of the fourth group is the variant:
v version (normally 4 for random GUIDs)
79faefbf-730a-0520-81f7-652183d4500b
^ variant (8, 9, a or b)
In A858's lists the variant is always 8, 9, a or b, like a normal GUID. The version is not 4, though:
it is a digit from 0 to 8, spread evenly. Every other character looks uniformly random. These are ordinary
random (v4) GUIDs, as Guid.NewGuid() makes them, whose version digit has been overwritten. That digit is the
mask.
The mask digits come from Random.Next(9)
Take all the GUIDs in one post and read off the version digit of each. For ifxpi (201107031600):
79faefbf-730a-0520-81f7-652183d4500b -> 0
101fa8b6-ef51-4c2c-b3a7-18dfdd76b981 -> 4
827d17ea-0a37-18a2-990d-461317c56b16 -> 1
3b33f45d-2784-0621-8a98-a1bd7bd93f08 -> 0
01298ccc-8734-1a15-a304-fb07025dba26 -> 1
...
The whole post is 80 GUIDs, so 80 digits:
04101007154885713683710134010462115554510158685657525768386507667266825716264134
This is exactly what new Random(527) gives when asked for 80 numbers from 0 to 8:
var random = new Random(527);
var str = new StringBuilder();
for (var i = 0; i < 80; ++i)
str.Append(random.Next(9));
Console.WriteLine(str);
// 04101007154885713683710134010462115554510158685657525768386507667266825716264134
This is not a coincidence: the chance that a random 80-digit string matches one of 1,000 seeds is about 1 in 1073. The same holds for:
- the plain July 2011 lists:
ifa54(seed 770),ifhho(603),ifhif(36, 128 GUIDs),ifxpi(527); - the decrypted inner layer of the 2012-06-26 → 2013-10-08 encrypted posts: every post with more than four GUIDs. Each post uses one seed, from its first GUID to its last;
- the list inside
o5o4o(seed 563),jkscb's grid (880),jkuyr(469),jbucr(494) andp5dsk(619); 1ywl20(seed 804), whose first GUID is lost in the CBC IV, so the match starts from the second draw.
Every post page on this site shows its mask digits with the seed that reproduces them, and the site build re-checks each seed against the decrypted list.
How it was found
Turning each decrypted inner-layer post (3,464 posts from 2012-06-26 to 2013-10-08, with more than four GUIDs) into a
digit string like this, the strings share their beginnings and never shift, so a shorter post is a prefix of a longer
one. That is what you would expect if each post is one stream from a generator with about 1,000 possible seeds. These
posts use 966 distinct seeds; by the birthday problem, 3,464 draws from 1,000 seeds gives about 969. A858 used .NET (the
PostAnalyzer source, and the .NET 4 executable in 14oru6), so the obvious generator was the
first one to try.
The seed is the current millisecond
The seeds are spread evenly over 0–999 and do not correlate with the title, length or anything else in the post. But in a steady run of posts, the seed changes smoothly from one post to the next. In February 2013 (run 42) A858 posted about every 66 minutes, and the seed goes down by roughly 50 each time:
| Post | Time (UTC) | GUIDs | Mask seed |
|---|---|---|---|
| 195ea8 | 2013-02-24 20:51:00 | 60 | 5 |
| 195iwq | 2013-02-24 21:57:00 | 48 | 896 |
| 195nos | 2013-02-24 23:03:00 | 40 | 851 |
| 195s2t | 2013-02-25 00:09:00 | 60 | 791 |
| 195wlp | 2013-02-25 01:15:00 | 56 | 746 |
| 19612s | 2013-02-25 02:21:00 | 44 | 679 |
| 1965mm | 2013-02-25 03:27:02 | 48 | 635 |
| 196a8p | 2013-02-25 04:33:00 | 36 | 589 |
| 196eed | 2013-02-25 05:39:00 | 48 | 529 |
| 196i1f | 2013-02-25 06:44:59 | 44 | 471 |
| 196kup | 2013-02-25 07:50:59 | 32 | 426 |
| 196n12 | 2013-02-25 08:56:59 | 40 | 364 |
(It wraps from 5 to 896 at the start, since it is mod 1000.) Across all the 2013 runs, the median change is
about −55 per hour, −142 over 3 hours and −310 over 6 hours. That is what you get from
new Random(DateTime.Now.Millisecond) inside a timer that fires on a fixed interval and runs slightly fast (about
14 ppm): the millisecond it fires at slowly drifts backwards. The same drift is visible in the post times, which slip
from :00 to :59 during a run.
The generator is therefore roughly:
var random = new Random(DateTime.Now.Millisecond);
for (var i = 0; i < count; ++i)
{
var guid = Guid.NewGuid();
// replace the version digit with random.Next(9)
...
}
What this means
The mask digit carries no message. It is a timestamp's millisecond fed through an RNG. The other 30 characters of
each GUID are Guid.NewGuid() output, which comes from the Windows crypto RNG and cannot be predicted or
reconstructed. So these lists, including the ~3,500 decrypted inner layers people spent years on, are filler. That
fits with 3j99nf (2015-09-01): "The treasure is amoung the trash."
The same generator also explains the six 2011 "32-bit group" posts (iirqk, iiafz, jqt3x, ihwxg, mymne,
n1ley). Each 4-byte word there is four Next(lo, hi) calls in a row from one millisecond-seeded Random, with a
fixed range for each byte: see the 32-bit word posts.
The number of GUIDs per post comes from a second, long-lived Random: see the count RNG.
Check it yourself
This Python script ports the .NET Framework Random (enough for seeds 0–999), fetches a post from Arctic Shift,
reads off the version digits and tries all 1,000 seeds. It works directly on the plain posts (ifa54, ifhho,
ifhif, ifxpi); for encrypted ones, use the decrypted list from the post's JSON file on this site.
import json, re, sys, urllib.request
def net_random(seed):
# .NET Framework System.Random(seed), enough for seeds 0..999
M = 2147483647
sa = [0] * 56
mj = 161803398 - seed
sa[55], mk = mj, 1
for i in range(1, 55):
ii = 21 * i % 55
sa[ii], mk, mj = mk, (mj - mk) % M, mk
for _ in range(4):
for i in range(1, 56):
sa[i] = (sa[i] - sa[1 + (i + 30) % 55]) % M
a, b = 0, 21
while True:
a = a % 55 + 1
b = b % 55 + 1
sa[a] = (sa[a] - sa[b]) % M
yield sa[a] / M
def mask_digits(seed, n):
rng = net_random(seed)
return ''.join(str(int(next(rng) * 9)) for _ in range(n))
post = sys.argv[1] if len(sys.argv) > 1 else 'ifxpi'
url = f'https://arctic-shift.photon-reddit.com/api/posts/ids?ids={post}&fields=selftext'
req = urllib.request.Request(url, headers={'User-Agent': 'a858-seed-check'})
text = json.load(urllib.request.urlopen(req))['data'][0]['selftext']
hexstr = re.sub(r'[^0-9a-fA-F]', '', text).lower()
guids = [hexstr[i:i + 32] for i in range(0, len(hexstr) - 31, 32)]
digits = ''.join(g[12] for g in guids)
print(len(guids), 'GUIDs, mask digits:', digits)
print('seeds:', [s for s in range(1000) if mask_digits(s, len(digits)) == digits])
Two runs in one post
if4fl (201107021123) was long the one masked list that no seed fits. Its 40 GUIDs are posted
as two blocks, 19 and 21, separated by a blank line, and the blank line is the answer: each block is a separate run
of the generator with its own Random. The first block's mask digits are new Random(360).Next(9) and the second's
are new Random(318).Next(9). Read as one list, the 40 digits mix two sequences, so no single seed can fit.
Both runs skip one draw before the first mask digit, while the other lists start at the first draw. What that
draw was for is not known. To check it with the script above, split the post at the blank line, take
mask_digits(seed, n + 1)[1:] and compare.