[WSG22] Daily Study Group: Cryptography (begins January 31!)

Another take on this, though it might be overkill for this problem. But let’s say that - for some reason - we don’t want to test all the possible Caesar shifts, for example if they are too costly

Then we can use the observation that the letter “l” is more frequent than the other letters:

text = "yhql ylgl ylfl";
CharacterCounts[text]

This shows that “l” is most frequent character.

<|"l" -> 5, "y" -> 3, " " -> 2, "f" -> 1, "g" -> 1, "q" -> 1,   "h" -> 1|>

What will be the first substitute to test, i.e. what is the commonest character in Latin?

Unfortunately, there is no letter frequency for Latin in WL:

LanguageData["Latin", "LetterFrequency"]

outputs Missing["NotAvailable"]

So we have to roll our own:

latinWords = WordList["KnownWords", Language -> "Latin"];
CharacterCounts[StringJoin[latinWords]]

which outputs the following, showing that the character “i” is the most common character in Latin.

<|"i" -> 6307, "e" -> 6008, "s" -> 5359, "a" -> 5324, "u" -> 5276, 
 "o" -> 5189, "r" -> 5013, "t" -> 4035, "n" -> 3395, "c" -> 3240, 
 "l" -> 2763, "m" -> 2490, "p" -> 2220, "d" -> 1609, "b" -> 1004, 
 "g" -> 946, "f" -> 896, "v" -> 869, "h" -> 469, "x" -> 409, 
 "q" -> 363, "y" -> 97, "æ" -> 94, "j" -> 51, "C" -> 38, "A" -> 34, 
 "H" -> 19, "M" -> 16, "z" -> 13, "I" -> 12, "T" -> 11, "P" -> 11, 
 "B" -> 10, "S" -> 9, "R" -> 8, "L" -> 8, "V" -> 6, "G" -> 6, 
 "*" -> 5, "O" -> 4, "F" -> 4, "N" -> 3, "E" -> 3, "D" -> 3, "k" -> 3,
  "Æ" -> 3, "Z" -> 2, "\[Hyphen]" -> 2, "U" -> 1, "-" -> 1, "Q" -> 1|>

Let’s now use this knowledge to first test the shift that replaces an “i” with “l”:

alpha = Alphabet\[\];
shift = LetterNumber["i"] - LetterNumber["l"];
keytable = Thread[Rule[alpha, RotateLeft[alpha, shift]]];
StringReplace["yhql ylgl ylfl", keytable]

Which happens to be the correct one:

veni vidi vici

Note: The shift found is -3 which is the same as shift 23 (26 + -3 = 23).

3 Likes

You’re welcome, I just notice the “A” out of place and tested the code. :slight_smile:

The SIGSALY used the heat of giant vacuum tubes as a source of noise, but I’ve also come
across how-to guides for using a reverse-biased zener diode to produce “shot noise”,
apparently something to do with total, natural randomness of quantum tunneling, don’t ask me!

[LINK] A Fast, Cheap, High-Entropy Source for IoT Devices

Thanks for mentioning this. It is possible to “deplete the entropy pool” when doing more business than usual, so HWRNG are worthwhile “weird machines”, which could be studied in their own right. How do you think graphene sheets could / should be adapted to this purpose? : :

Vacuum tubes can still be used for some fun experiments on the photoelectric effect, or if you have excess medical grade equipment, possibly for making Geiger counters.

However, as you’ve said, much of more recent technology has already shifted toward diodes and laser diodes, which are also incredible little devices for any hobbyist’s citizen lab. Have Fun! --Brad

2 Likes

This is a somewhat simple-minded check for English words which result in other English words when encrypted with the Caesar Cipher (and only using a shift of 3 characters). It considers a word valid if it is contained in the “common words” returned by WordList[]. The results would be different if DictionaryWordQ[] was used to validate the result.

CCipher[x_String, offset_Integer : 3] := 
 Mod[LetterNumber[x] + offset - 1, 26] + 1 // FromLetterNumber //   StringJoin
Attributes[CCipher] = Listable;
CCipher[WordList[]]\
    // Intersection[#, WordList[]] & \
    // Thread[CCipher[#, 23] -> #] & \

{"alb" -> "doe", "ark" -> "dun", "be" -> "eh", "box" -> "era", 
 "bob" -> "ere", "bod" -> "erg", "boo" -> "err", "cold" -> "frog", 
 "dory" -> "grub", "ex" -> "ha", "elm" -> "hop", "ere" -> "huh", 
 "fa" -> "id", "fob" -> "ire", "folk" -> "iron", "ill" -> "loo", 
 "lab" -> "ode", "lob" -> "ore", "milt" -> "plow", "molt" -> "prow", 
 "orb" -> "rue", "pee" -> "shh", "pelt" -> "show", "perk" -> "shun", 
 "ply" -> "sob", "pry" -> "sub"}

Update:

After I posted this code, I read Michael Schreiber’s solution which used StringReplace rather than LetterNumber and FromLetterNumber and wondered about the relative efficiency of the approaches. I changed my code to use Michael’s approach and the the speed increased by a factor of more than 60. Thank you Michael for demonstrating this approach.

Updated code:

rules = Thread[Rule[Alphabet[], RotateLeft[Alphabet[], offset]]];
rulesRev = Thread[Rule[Alphabet[], RotateLeft[Alphabet[], 26 - offset]]];
wl = WordList[] // ToLowerCase;
StringReplace[#, rulesRev] -> # & /@ 
Intersection[wl, StringReplace[wl, rules]]
2 Likes

I came, saw and won.

A longer list. DictionaryWordQ has more words than WordList.

WordList[] // 
AssociationMap[ResourceFunction["CaesarCipher"][#, 3] &] // 
Select[DictionaryWordQ]
1 Like

As a question/suggestion: isn’t it common to define the term “information security system” for the set of properties i.e. confidentiality, authentication, data integrity, and non-repudiation.

This question may be more related to a future crypto lesson.

I was toying around with encrypting/decrypting data (using Mathematica 12.1 functions) with DES, using the “Electronic Cookbook” (ECB) block mode, and was surprised by some obtained results, see below.
I would like to know the reasons of such behaviour, as well as whether it’s possible to know and/or control what data padding is being used by default, in these settings?

For an (a priori) empty byte array, the encrypted data length is much longer than for a one-byte array:

In[1]:= Encrypt[desKeyECB, ByteArray[{}]]["Data"] // Normal
Out[1]= {238,110,253,99,228,27,8,224,142,28,87,146,1,47,70,240,237,69,176,169,25,122,166,240}

but:

In[2]:= Encrypt[desKeyECB, ByteArray[{0}]]["Data"] // Normal
Out[2]= {177,216,132,168,6,11,203,72}

which is shorter.

Finally, while it is known that DES works on 64-bit data chunks, I noticed that the encrypted data’s length will increase already the 56-bit

7-byte (i.e. 56-bit)-long data: gives an encrypted block of 64 bits:

In[3]:= Encrypt[desKeyECB, ByteArray[{0, 1, 2, 3, 4, 5, 6}]]["Data"] // Normal
Out[3]= {62,54,162,174,211,50,208,182}

However, 8-byte (i.e. 64-bit)-long data would give a longer encrypted block of 2 x 64 bits:

In[4]:= Encrypt[desKeyECB, ByteArray[{0, 1, 2, 3, 4, 5, 6, 7}]]["Data"] // Normal
Out[4]= {83,131,254,213,181,242,11,15,151,186,160,29,255,217,246,70}

Interestingly, the last 8 bytes in the example above appear to “encode” an empty byte array somehow:

In[5]:= Decrypt[desKeyECB, ByteArray@{151, 186, 160, 29, 255, 217, 246, 70}]
Out[5]= {}

But the empty byte array encrypts differently (see In[1] above).
Finally, modifying one of these 8 bytes in In[5] would make Decrypt[] fail. Why is that so? Does this padding also contain some sort of checksum information?

What you see in In[1] is due to the fact that in Wolfram an empty byte array is actually a list:

In[1]:= ByteArray[{}]

Out[1]= {}

In[2]:= ByteArray[{}] == List[]

Out[2]= True

So since in WL you can encrypt any expression not just bytes or strings this gets treated as expression List[] and serialized as a generic expression. If you really want to encrypt an empty thing try empty string “”.
What you see in the inputs 3, 4, and 5 is related to a standard convention on padding, which will be covered in lesson “Block and stream ciphers next week” so I hope you wouldn’t mind waiting for that.

3 Likes

Earlier this week Linear Congruential Generators (LCG) was mentioned and I was trying to solve/attack the simple example that was mentioned in the notebook (09_Randomness_and_psedorandomness.nb), but was not able to figure out how to restore the parameters a, b, and m in the LCG function.

Here’s the definition of the LCG from the notebook and the first 10 generated numbers in the sequence:

a = 65539;
b = 0;
m = 2^31;
LCG[seed_Integer] := Mod[a*seed + b, m];

NestList[LCG, 1, 10]

(the output:)

{1, 65539, 393225, 1769499, 7077969, 26542323, 95552217, 334432395, \
1146624417, 1722371299, 14608041}

First some simple trials of getting back the used values of a , b, and m using different approaches that doesn’t work:

FindGeneratingFunction[seq, x]

returns the following (which means that it cannot find a solution):

FindGeneratingFunction[{1, 65539, 393225, 1769499, 7077969, 26542323, 
  95552217, 334432395, 1146624417, 1722371299, 14608041}, x]

But I would have thought that either Solve, Reduce or FindInstance could give some answer:

Solve[{seq[[2]] == Mod[aa * seq[[1]] + bb, mm]
    }, {aa, seed, bb, mm} \[Element] Integers]

Nope, it returns:

This system cannot be solved with the methods available to Solve.

What about a little larger equation system:

Solve[{seq[[2]] == Mod[aa * seq[[1]] + bb, mm],
  seq[[3]] == Mod[aa * seq[[2]] + bb, mm],
  seq[[4]] == Mod[aa * seq[[3]] + bb, mm],
  seq[[5]] == Mod[aa * seq[[4]] + bb, mm],
  seq[[6]] == Mod[aa * seq[[5]] + bb, mm]
    }, {aa, seed, bb, mm} \[Element] Integers]

Same error/warning message.

I also tested this with Reduce and FindInstance but they all errors/warnings

Reduce::nsmet: This system cannot be solved with the methods available to Reduce.

and

FindInstance::nsmet: The methods available to FindInstance are insufficient to find the requested instances or prove they do not exist.

I assume it is the Mod function that makes this such a hard problem, or perhaps I just did some simple (or not so simple) mistake.

Are there some good principled ways to solve/attack LCGs (in WL)?

1 Like

This group might be interested in a post a made a few months ago Twitter steganography .

The interesting use for steganography may be to send encrypted information attached to Twitter photos. As a communication channel it might be very hard to monitor and notice when encrypted information was actually being transmitted.

3 Likes

Just a small thing I noticed in the course notebook 11_Security features in the section with the collision for different hexadecimals hashed with MD5.
SameQ was returning True regardless of what type of hash I was choosing. I think it works when implemented with “@@” on the list returned from the hashing function. i.e. the SHA256 hashes are different.

In[17]:= SameQ[Hash[BaseDecode[#, "Base16"], "SHA256"] & /@ {hex1, hex2}]

Out[17]= True

In[18]:= SameQ@@(Hash[BaseDecode[#, "Base16"], "SHA256"] & /@ {hex1, hex2})

Out[18]= False

The MD5 hash is still identified as identical.

In[20]:= SameQ@@(Hash[BaseDecode[#, "Base16"], "MD5"] & /@ {hex1, hex2})

Out[20]= True
3 Likes

Three years of Latin in high school helped confirm my decryption and translation. Good fun!

Thanks for finding this!

In[120]:= SameQ[a, b]

Out[120]= False

In[118]:= SameQ @@ {a, b}

Out[118]= False

In[119]:= SameQ[{a, b}]

Out[119]= True

In[117]:= SameQ@{a, b}

Out[117]= True

1 Like

Initialization cell to introduce the abbreviation seq :wink:

a = 65539; b = 0; m = 2^31; LCG[seed_Integer] := Mod[a*seed + b, m]; seq = NestList[LCG, 1, 10]

Hello Hakan,

If you “help” WL by specifying the bb = 2^31 and the shift bb = 0, you will get an answer:

Solve[{seq[[2]] == Mod[aa*seq[[1]] + 0, 2^31]}, aa \[Element] 
Integers]

Even if you omit the shift bb = 0, you get an answer.

Solve[{seq[[2]] == Mod[aa*seq[[1]] + bb, 2^31]}, {aa, bb} \[Element] 
  Integers]

On the other hand, if you ask for mm, i.e.

Solve[{seq[[2]] == Mod[aa*seq[[1]] + 0, mm]}, {aa, mm} \[Element] 
  Integers]

your error/warning message will be reproduced. The same happens when you try the toy example

Solve[1 == Mod[15, x], x]

(Edit: Note, that Solve[1 == Mod[15, x], x, Integers] works, and likewise Solve[2022 == Mod[65539, x], x, Integers]. \Edit)

But you can get around that by running this:

Table[{k -> 
   Solve[{seq[[2]] == Mod[aa*seq[[1]] + 0, 2^k]}, {aa} \[Element] 
     Integers]}, {k, 1, 40}]

By doing so, you get a whole bunch of solutions…

But I think we’re moving from the Crypto topic to modulo computation now…

PS: Sorry I didn’t post the output here, because the conditional expressions make the result very unreadable here…

2 Likes

Thanks for your insights and suggestions , Lara!

Perhaps it make sense that hard coding the value of the modulo works, it might be given in the specification of the used algorithm?

Some findings:
Using the hard coded modulo with a single equation takes about 34s on my computer. But if adding one more equation to the system it’s faster: about 7s.

Solve[{seq[[2]] == Mod[aa*seq[[1]] + bb, 2^31], 
       seq[[3]] == Mod[aa*seq[[2]] + bb, 2^31]
   }, {aa, bb}, Integers] // AbsoluteTiming

Adding more equations seems to be a little slower: 3 equations takes 7.4s, 4 equations takes 7.5s and 5 equations takes 8s.

But there are faster ways:

Using Reduce only takes 0.05s:

Reduce[{seq[[2]] == Mod[aa*seq[[1]] + bb, 2^31]}, {aa, bb}, Integers] // AbsoluteTiming

[FindInstance is not that fast and give solutions “all over the place” and I had to add the constraints that aa and bb are non-negative. Here we want 5 solutions.

FindInstance[{seq[[2]] == Mod[aa*seq[[1]] + bb, 2^31], aa >= 0, bb >= 0}, 
             {aa, bb}, Integers, 5] // AbsoluteTiming

Solutions:

{0.467047, {{aa -> 210453397504, 
   bb -> 785979080717}, {aa -> 188978626658, 
   bb -> 689342250923}, {aa -> 0, 
   bb -> 193273593869}, {aa -> 141733986317, 
   bb -> 0}, {aa -> 128849084491, bb -> 575525617602}}}

]

I like the idea of looping through the modulo (2^k) though that assumes that the modulo is always a power of 2, which might very well be correct.

Regarding your comment that looping through possible modulo values is leaving the cryptanalysis domain, I would say that we are still in that domain since everything is allowed to get a solution (well , perhaps except for a Kevin Mitnick kind of social engineering :-)).

I had hoped that Solve would be of some (more) help here.

Again, thanks!

2 Likes

Here is a notebook exploring if some of WL’s simpler hash functions generates hash collisions for some of WL’s wordlists.

4 Likes

Note that this is not a very serious crypto analysis of hash
collisions since we know that these hash functions are not secure (and
some of them are not designed to be crypto secure).

As you also mentioned above, Adler32 is not a serious hash function. It is easy to study from first principles and find how to design a collision algorithm. This was done on a previous thread “Hello World = Goodbye World: chosen-prefix collision attack” (editors note: we have since learned that the word “attack” is better replaced by “analysis”).

I did not put very much thought or effort into the above, so it would be interesting to see who can improve these results and how. If anyone has a good MD5 collider, that might make a good contribution to Wolfram Function Repository. Happy searching!

2 Likes

Brad, thanks for the link of your analysis. I’ll read it more carefully and also the “SHA-1 is a Shambles” article you cited (https://sha-mbles.github.io/ ).

A related idea is to search for the smallest hash collisions for the two weak hash functions (CRC32 and Adler32) and use all possible strings of a certain size. Unfortunately, for the generated wordlists that I could test, there wasn’t any hash collisions which surprised me a little. Anyway, here’s the idea and the code.

Let’s assume that we use words with strings of the characters [a-zA-Z0-9] and generate all possible strings up to a certain length, i.e. size = 3 generates all possible strings of length 1, 2, and 3.

Here’s the alphabet and 30 random samples:

alpha = Flatten[{CharacterRange["a", "z"], CharacterRange["A", "Z"], 
   CharacterRange["0", "9"]}];

RandomSample[
 Table[StringJoin[#] & /@ Tuples[alpha, n], {n, 1, 3}] // Flatten, 30]

{"oEg", "LMZ", "j3n", "GDY", "dUe", "7ma", "Z5x", "jtb", "6OI", \
"HdL", "EzE", "VMY", "OYM", "p7Z", "PNg", "Vqz", "obw", "Kyj", "OUi", \
"pda", "LwI", "S2A", "4KO", "Ah0", "VsX", "XI4", "ZiC", "tRE", "EMv", \
"fc3"}

We reuse some of the functions from the original notebook:

crc32[word_] := Hash[word, "CRC32", "HexString"]
adler32[word_] := Hash[word, "Adler32", "HexString"]

listCollisions2[words_, fun_] := 
 Module[{hashList, counts, hashMap, collisions},
  hashList = fun[#] & /@ words;
  If[Length[hashList] != Length[words],
   counts = Counts[hashList];
   hashMap = AssociationThread[words -> hashList];
   collisions = Keys@Select[counts, # > 1 &];
   Table[Select[hashMap, # == c & ], {c, collisions}] ,

   {} (* else: no collisions *)
   ]
  ]

As mentioned above, we don’t found any hash collisions for CRC32 or Adler32 for sizes up to 4:

Table[{n, listCollisions2[generateWords[n], crc32]}, {n, 1, 4}] // AbsoluteTiming
{25.0142, {{1, {}}, {2, {}}, {3, {}}, {4, {}}}}

Table[{n, listCollisions2[generateWords[n], adler32]}, {n, 1, 4}] // AbsoluteTiming
{25.9066, {{1, {}}, {2, {}}, {3, {}}, {4, {}}}}

One major problem with this approach is that the size of wordlists increases very fast and soon be forbiddingly large:

Table[{n, Length[generateWords[n]]}, {n, 1, 4}] // TableForm

{
 {1, 62},
 {2, 3 906},
 {3, 242 234},
 {4, 150 18 570}
}

For size 5, I couldn’t even generate the wordlist (got the error “No more memory available. Mathematica kernel has shut down”), but using WL’s excellent FindSequenceFunction we can at least estimate the sizes of the wordlists for larger sizes:

FindSequenceFunction[{62, 3906, 242234, 15018570}]
{#, %[#]} & /@ Range[1, 10] // TableForm

which outputs

62/61 (-1 + 62^#1) &
{
 {1, 62},
 {2, 3906},
 {3, 242234},
 {4, 15018570},
 {5, 931151402},
 {6, 57731386986},
 {7, 3579345993194},
 {8, 221919451578090},
 {9, 13759005997841642},
 {10, 853058371866181866}
}

Thus, generateWords[5] would give a wordlist of 931 151 402 words.

Update:

Perhaps it’s not that surprising that there are no hash conflicts for Adler32. Here are some of the words and their Adler32 hashes for the size 4 generated words, i.e. the hashes are distinct and just incrementing from the last (two counters) and are in order.

Short[{#, adler32[#]} & /@ generateWords[4], 20]

{{"a", "00620062"}, {"b", "00630063"}, {"c", "00640064"}, {"d", 
  "00650065"}, {"e", "00660066"}, {"f", "00670067"}, {"g", 
  "00680068"}, {"h", "00690069"}, {"i", "006a006a"}, {"j", 
  "006b006b"}, {"k", "006c006c"}, {"l", "006d006d"}, {"m", 
  "006e006e"}, {"n", "006f006f"}, {"o", "00700070"}, {"p", 
  "00710071"}, {"q", "00720072"}, {"r", "00730073"}, {"s", 
  "00740074"}, {"t", "00750075"}, {"u", "00760076"}, {"v", 
  "00770077"}, {"w", "00780078"}, {"x", "00790079"}, {"y", 
  "007a007a"}, {"z", "007b007b"}, {"A", "00420042"}, {"B", 
  "00430043"}, {"C", "00440044"}, {"D", "00450045"}, {"E", 
  "00460046"}, {"F", "00470047"}, <<15018506>>, {"999E", 
  "024a00f1"}, {"999F", "024b00f2"}, {"999G", "024c00f3"}, {"999H", 
  "024d00f4"}, {"999I", "024e00f5"}, {"999J", "024f00f6"}, {"999K", 
  "025000f7"}, {"999L", "025100f8"}, {"999M", "025200f9"}, {"999N", 
  "025300fa"}, {"999O", "025400fb"}, {"999P", "025500fc"}, {"999Q", 
  "025600fd"}, {"999R", "025700fe"}, {"999S", "025800ff"}, {"999T", 
  "02590100"}, {"999U", "025a0101"}, {"999V", "025b0102"}, {"999W", 
  "025c0103"}, {"999X", "025d0104"}, {"999Y", "025e0105"}, {"999Z", 
  "025f0106"}, {"9990", "023500dc"}, {"9991", "023600dd"}, {"9992", 
  "023700de"}, {"9993", "023800df"}, {"9994", "023900e0"}, {"9995", 
  "023a00e1"}, {"9996", "023b00e2"}, {"9997", "023c00e3"}, {"9998", 
  "023d00e4"}, {"9999", "023e00e5"}}

Testing with some longer words and it use the same principle of two counters:

{#, adler32[#]} & /@ {"aaaaa", "aaaab", "aaaac", "aaaaaaaaaaa", 
  "aaaaaaaaaab",
  "aaaaaaaaaaaaaaaaaaa", "aaaaaaaaaaaaaaaaaab"}

{{"aaaaa", "05b401e6"}, {"aaaab", "05b501e7"}, {"aaaac", 
  "05b601e8"}, {"aaaaaaaaaaa", "190d042c"}, {"aaaaaaaaaab", 
  "190e042d"}, {"aaaaaaaaaaaaaaaaaaa", 
  "48110734"}, {"aaaaaaaaaaaaaaaaaab", "48120735"}}

The hashes for CRC32 is much more random like:

Short[{#, crc32[#]} & /@ generateWords[4], 20]

{{"a", "e8b7be43"}, {"b", "71beeff9"}, {"c", "06b9df6f"}, {"d", 
  "98dd4acc"}, {"e", "efda7a5a"}, {"f", "76d32be0"}, {"g", 
  "01d41b76"}, {"h", "916b06e7"}, {"i", "e66c3671"}, {"j", 
  "7f6567cb"}, {"k", "0862575d"}, {"l", "9606c2fe"}, {"m", 
  "e101f268"}, {"n", "7808a3d2"}, {"o", "0f0f9344"}, {"p", 
  "82079eb1"}, {"q", "f500ae27"}, {"r", "6c09ff9d"}, {"s", 
  "1b0ecf0b"}, {"t", "856a5aa8"}, {"u", "f26d6a3e"}, {"v", 
  "6b643b84"}, {"w", "1c630b12"}, {"x", "8cdc1683"}, {"y", 
  "fbdb2615"}, {"z", "62d277af"}, {"A", "d3d99e8b"}, {"B", 
  "4ad0cf31"}, {"C", "3dd7ffa7"}, {"D", "a3b36a04"}, {"E", 
  "d4b45a92"}, {"F", "4dbd0b28"}, <<15018506>>, {"999E", 
  "8fef8e8d"}, {"999F", "16e6df37"}, {"999G", "61e1efa1"}, {"999H", 
  "f15ef230"}, {"999I", "8659c2a6"}, {"999J", "1f50931c"}, {"999K", 
  "6857a38a"}, {"999L", "f6333629"}, {"999M", "813406bf"}, {"999N", 
  "183d5705"}, {"999O", "6f3a6793"}, {"999P", "e2326a66"}, {"999Q", 
  "95355af0"}, {"999R", "0c3c0b4a"}, {"999S", "7b3b3bdc"}, {"999T", 
  "e55fae7f"}, {"999U", "92589ee9"}, {"999V", "0b51cf53"}, {"999W", 
  "7c56ffc5"}, {"999X", "ece9e254"}, {"999Y", "9beed2c2"}, {"999Z", 
  "02e78378"}, {"9990", "af800b3e"}, {"9991", "d8873ba8"}, {"9992", 
  "418e6a12"}, {"9993", "36895a84"}, {"9994", "a8edcf27"}, {"9995", 
  "dfeaffb1"}, {"9996", "46e3ae0b"}, {"9997", "31e49e9d"}, {"9998", 
  "a15b830c"}, {"9999", "d65cb39a"}}
2 Likes