Package: cryptonita
cryptonitais a set of
building blocks to create automated crypto-attacks.You may not find the advanced attack implemented here (yet) but I hope
that this building blocks or primitives can help you in your journey.Without more, let’s put our hands on and break the famousVigenere
cipher.Tutorial - Break a xor Vigenere cipherTheVigenere
cipherwas once
the most secure cipher. It was thought that it was unbreakable…Let’s put under test that statement and learn aboutcryptonitaalong the journey!Note: the following README is also an automated test for thecryptonitalib thanks tobyexample.Implement the cipher - Load the bytesThe building block incryptonitais thebyte string: a finite
immutable sequence of bytes.Incryptonitawe can create abyte stringwith theBfunction
and do any conversion needed:>>>fromcryptonitaimportB# byexample: +timeout=10>>>B(u'from an unicode encoded text',encoding='utf-8')'from an unicode encoded text'>>>B([0x46,0x72,0x6f,0x6d,0x20,0x6e,0x75,0x6d,0x62,0x65,0x72,0x73])'From numbers'>>>B('RnJvbSBiYXNlNjQ=',encoding=64)'From base64'For our purposes of implementing a Vigenere cipher, let’s load some
plain text from a file:>>>ptext=B(open('./test/ds/plaintext','rt').read())>>>ptext[:29]'Now that the party is jumping'For the full list of conversions seecryptonita/conv.py’s
``as_bytes`<https://github.com/cryptonitas/cryptonita/tree/master/cryptonita/conv.py>`__Implement the cipher - Apply a xorFirst, we load our secret key in base 64 from the file. Notice how the
decoding from base 64 is made byB:>>>secret=B(open('./test/ds/secret','rt').read(),encoding=64)The Vigenere cipher consists in xord the plaintext with the key. If the
plaintext is larger than the key, just repeat the key over and over.cryptonitacan do exactly that:>>>ctext=ptext^secret.inf()>>>ctext[:29].encode(64)b'OA4ZSRgEAAJBGgEJTBEXExoQTAUSVgsbBBwFDxE='Theinf()method tells that thesecretstring must be seen as an
“infinite sequence”, repeating the key over and over.Then, the^just does the xor byte by byte.For the full list of operation onImmutableByteStringseecryptonita/bytestrings.py’s
``ImmutableByteString`<https://github.com/cryptonitas/cryptonita/tree/master/cryptonita/bytestrings.py>`__
and themixinsBreaking the cipher - Scoring the length of the keyVigenere was thought to be unbreakable because a priori is not possible
to know the length of the key.However this was proved to be false.In 1863,Kasiskicame with a cleaver method to know the length of the key but it is quite
hard to make it right and faster (I’m stillworking on
it)Modern and better approaches are theHamming
distanceand theIndex of
CoincidenceThe idea is to assume that the key is of length L and then pick every
Lth byte of the ciphertext:>>>L=8# totally arbitrary here>>>picked=ctext[::L]Note how theImmutableByteStringciphertext supports indexing
operation like any Python string.Now we compute the Index of Coincidence (IC) of this picked string.If the assumed length L isnotthe correct one, every picked byte
will be the xor of the plaintext with a different key byte and the wholepickedstring would likerandomand the IC will be very low.On the other hand, if we guessed correctly the length L,allthe
picked bytes will be the xord of the plaintext and thesamekey byte
and therefore will not look random. A high IC would be expected!>>>fromcryptonita.metricsimporticoincidences>>>icoincidences(picked)0.02<...>Seecryptonita/scoring.pyandcryptonita/metrics.pyI you want to know more about the Index of Coincidence see thisblog
postabout it and thiscomparison with other
methodsBreaking the cipher - Guessing the length of the keyA IC of 0.02 is too low. It seems that 8 is not the length of the key.We could do a loop to try other lengths butcryptonitaalready has
that>>>fromcryptonita.scoringimportkey_length_by_ic>>>fromcryptonita.attacksimportguess_key_length>>>gklength=guess_key_length(...ctext,...length_space=range(5,25),...score_func=key_length_by_ic,...min_score=0.025...)Okay, what is that?guess_key_lengthdoes a brute forceattackcomputing ascore
functiontesting every possible length from 5 to 25.key_length_by_icis ascoreshow good the tested length is. It
puts a score between 0 (bad) and 1 (good) using the Index of
Coincidence (like theicoincidences).You may think thatgklengthis thetheguessed key but in
cryptoanalysis you mostly never work with aspecificvalue. You work
with aset of possible values.>>>gklength{5:0.02702702702702703,6:0.027649769585253458,7:0.04682040531097135,8:0.02682701202590194,9:0.025551684088269456,10:0.025604551920341393,12:0.038306451612903226,14:0.03133903133903134,16:0.028985507246376812,17:0.02766798418972332,21:0.032679738562091505,24:0.041666666666666664}Incryptonitawe call these sets, theseguesses,FuzzySet.For more scoring functions seecryptonita/scoring.pyBreaking the cipher - A guess as a fuzzy setA guess orFuzzySetis a bunch of possible solutions, each with an
associated probability or score.We can query then the most likely answer. In our case, the most likely
length of the key:>>>gklength.most_likely()7But the most likely may not necessary mean the correct answer. Instead,
you should work always with the fuzzy set to test all of them.If the sets gets to large (and they will), you can cut them off,
dropping items with a probability lower than some threshold.Here we say that any length with a lower probability of 0.01 should be
out:>>>gklength.cut_off(0.03)>>>gklength{7->0.0468,24->0.0417,12->0.0383,21->0.0327,14->0.0313}Take a look at thedocumentation of
``FuzzySet`<https://github.com/cryptonitas/cryptonita/tree/master/cryptonita/fuzzy_set.py>`__
and optional a wiki aboutfuzzy set
theory.Breaking the cipher - Chop the ciphertext into blocksNow the we have a set of possible lengths, here is the plan to crack the
cipher:First, split the ciphertext intoblocksof guessed length L:>>>L=gklength.most_likely()>>>cblocks=ctext.nblocks(L)ciphertext:  ABCDEFGHIJKLMN
              |   |    |  |
              |   |    \  \___
              |   |     \     \
cblocks      ABCD  EFGH  IJKL  MNEach first byte of those blocks are supposedly the result of xor the
plaintext with the same key byte. The same goes for the second byte of
each block and so on.Second, because it is easier to have all the first bytes in one block,
all the second bytes in another block and so on, we want totransposethe blocks:>>>fromcryptonita.convimporttranspose>>>cblocks=transpose(cblocks,allow_holes=True)cblocks   --> transposed cblocks
 ABCD           AEIM
 EFGH           BFJN
 IJKL           CGK
 MN             DHLNow, each block (or row) is a piece of plaintext encrypted with the same
single-byte key.Let’s break it!Breaking the cipher - Frequency attackWe could test all the 256 possible byte keys by brute force but that’s
quite slow.Rather we could do afrequency attackbecause the statistics of the
plaintext are leaked into the ciphertext.cryptonitaalready provides us with a very simplemodelof the
frequencies of the English plaintext: the famousETAOIN SHRDLU.>>>fromcryptonita.scoring.freqimportetaoin_shrdluIf our ciphertext has the same distribution than the plaintext, at least
one of the most common bytes in the ciphertext should be one of the most
common bytes in the plaintext, encrypted of course.Under this hypothesisfreq_attackxor the top most common bytes in
the ciphertext with the most common bytes in plaintext according to the
model.>>>most_common_pbytes=etaoin_shrdlu()>>>ntop_most_common_cbytes=1>>>fromcryptonita.attacksimportfreq_attack>>>freq_attack(cblocks[0],most_common_pbytes,ntop_most_common_cbytes){'"':0.07387790762504176,'$':0.055504740275805896,'%':0.0561520934139066,'2':0.03178778752478832,'3':0.10384587375686015,'5':0.026296157563462763,'7':0.07060615929878336,'8':0.060837928943597436,'9':0.0634364224946222,':':0.0342469273170487,'>':0.03964865941609311,'?':0.06072776315086166,'v':0.17269159612928756}In general,freq_attackcannot give usthebyte key but it can
give use aguess: a fuzzy set of possible keys. This is a much shorted
list than 256!But don’t claim victory yet. We broke only the first block
(cblocks[0]).More frequency models may be found atcryptonita/scoring/freq.pyBreaking the cipher - Guess explosionWe need to callfreq_attackfor all the blocks:>>>gbkeys=[]>>>forcincblocks:...gbkeys.append(freq_attack(c,most_common_pbytes,ntop_most_common_cbytes))>>>len(gbkeys)7So we have 7 guesses (7 fuzzy sets), one guess set per byte of the key.But the key is one of theall possible combination of the guesses.How many possible keys do we have?>>>fromcryptonita.fuzzy_setimportlen_join_fuzzy_sets>>>len_join_fuzzy_sets(gbkeys)62748517How! that’s a lot! But stillmuch less than256^7 which is greater
than the age of theobservable
universein
years.Still, we need to shrink the guesses even further to make it manageable.Breaking the cipher - Brute force refinementfreq_attackis really powerful but it is not the only tool that we
have.Not all the possible keys in a guess will produce“reasonable”plaintext.We canscorea plaintext and filter out the ones that don’t look“good enough”cryptonitaimplements different scoring functions andall_ascii_printableis the most simplest to understand:Let’sassumethat the plaintext is an English message encoded in
ASCII.If we decipher one block and we got a plaintext with non-printable ASCII
char we can be sure that the key used is incorrect and we can score it
with a0. Otherwise, we score it with1.>>>fromcryptonita.scoringimportall_ascii_printable>>>all_ascii_printable(B("a reasonable plaintext"))1>>>all_ascii_printable(B("n\0t v\4lid"))0The plan is to tryallthe possible byte keys ineachof our
guesses, score the results and drop the ones with lower score.>>>fromcryptonita.attacksimportbrute_force>>>fori,cinenumerate(cblocks):...# the fuzzy set of keys (a guess) for this ith byte...gbkey=gbkeys[i]......refined=brute_force(c,...score_func=all_ascii_printable,...key_space=gbkey,...min_score=0.01...)......# "refined" is another fuzzy set (a guess) for the ith byte...# but probably a much smaller one...gbkeys[i]=refinedLikeguess_key_length,brute_forcereceives a score function, a
key space and a minimum score.Now we have a much smaller search space to work on:>>>len_join_fuzzy_sets(gbkeys)260>>>260/627485174.14<...>e-06While still we have a lot of possible keys, the refinement did an
amazing job and the new set is6 orders of magnitud smallerthan the
original!We can compute the set of possible keys doing a join and we can even
further reduce the set keeping only the most likely keys:>>>fromcryptonita.fuzzy_setimportjoin_fuzzy_sets>>>gkstream=join_fuzzy_sets(gbkeys,cut_off=1024,j=B(''))gkstreamis our guess for the complete key stream for the cipher.Is this right?Breaking the cipher - Break the cipher!>>>kstream=gkstream.most_likely()>>>print((ctext^kstream.inf()).decode('ascii'))NowthatthepartyisjumpingWiththebasskickedinandtheVega's are pumpinQuicktothepoint,tothepoint,nofakingCookingMC's like a pound of baconBurning'em, if you ain'tquickandnimbleIgocrazywhenIhearacymbalAndahighhatwithasoupeduptempoI'm on a roll, it'stimetogosoloollin' in my five point ohithmyrag-topdownsomyhaircanblow>>>kstream.encode(64)b'dmFuaWxsYQ=='Final thoughtsVigenere or a repeating key cipher is a well known poor cipher shown in
every single cryptography course.But little is explained in how to break it in anautomatedfashion.cryptonitais not magical and
a little of brain is required from you, but it is a quite usefulSwiss
army knife for breaking crypto.PRs or comments are welcome.Tested withbyexample.