Challenge 8a done, 8b not attempted
authorNeil Smith <neil.git@njae.me.uk>
Wed, 6 Jan 2016 20:52:58 +0000 (20:52 +0000)
committerNeil Smith <neil.git@njae.me.uk>
Wed, 6 Jan 2016 20:52:58 +0000 (20:52 +0000)
2015-challenge8.ipynb [new file with mode: 0644]
2015/8a.ciphertext [new file with mode: 0644]
2015/8b.ciphertext [new file with mode: 0644]
2015/Solitaire.py [new file with mode: 0644]
2015/cards2.png [new file with mode: 0644]
2015/sol.py [new file with mode: 0644]
SIGNED.md

diff --git a/2015-challenge8.ipynb b/2015-challenge8.ipynb
new file mode 100644 (file)
index 0000000..90231b1
--- /dev/null
@@ -0,0 +1,93 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "import matplotlib.pyplot as plt\n",
+    "%matplotlib inline\n",
+    "\n",
+    "from cipherbreak import *\n",
+    "\n",
+    "c8a = sanitise(open('2015/8a.ciphertext').read())\n",
+    "c8b = sanitise(open('2015/8b.ciphertext').read())"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "('charlie', -2104.8140749325567)"
+      ]
+     },
+     "execution_count": 12,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "key_a, score = vigenere_frequency_break(c8a)\n",
+    "key_a, score"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "harry i am so sorry we went into the russian hq last night without you but the station chief wanted it to be his team that got the glory i am really grateful to you for coming back and putting us on the right track though the stolen file from soviet headquarters was as you expected encrypted with the solitaire cipher fortunately the cipher clerk who managed the encryption was incredibly careless i found a sheet of burnt paper in the bin which gave me a list of thirty eight cards and i am hoping that this is a large part of the key it will still be hard to break but maybe not impossible especially as the erased part was still intact there maybe another clue in that the page appears to have been torn from an economics textbook i found on the desk anyway i figure the chemists at langley may help us to reconstruct the whole key given time though i wouldnt expect them to manage more than one card a day given how careful they will have to be not to destroy the document it will take usa while to crack this but maybe time is on our side with christmas things seem to be quiet and i am hoping that within the next three weeks we may know precisely what the soviets were trying to do here whatever the outcome i think it is clear that the future of europe is not likely to be settled for a while i hear rumours everyday about shortages in the soviet bloc and border controls are going up in places you wouldnt expect to prevent largescale migration there are problems in greece and turkey and divisions between the british and french and the brits are having real trouble paying off their war debts whether or not we crack the reichs doktor mystery i think there is going to be plenty for you to do i know we had to work hard to persuade you to flyover but we really do need you here even the chief recognizes that so if i can i want to persuade you to stay francois and i are being posted to paris kind of a thankyou for our work on this project but icant go unless i know the berlin station has someone i trust hope youll agree to take the job charlie\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(' '.join(segment(vigenere_decipher(c8a, key_a))))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.4.3"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 0
+}
diff --git a/2015/8a.ciphertext b/2015/8a.ciphertext
new file mode 100644 (file)
index 0000000..de67fa8
--- /dev/null
@@ -0,0 +1 @@
+JHRIJQEOZOJZZVADENPVXKUTFEPITBSJTIRJXLRDBRKNHKHQXJVUKJWYDBTKSMWVHTZZVGJPEWHIRVLDZEBSDLHZDBICTTYLBKQATYPOPQYYZLUVGHLCJOVCAEWFTXQFOLQWVEVMZYOFCJKRYLTWATZYOYUVNKSMVKNHKEZEERTYZCKJAHVDBSNLNWTTIHYODDWZKLTYPIHSBAIEMVUDAJLACQBEOAMGVLDVYKVAWTVOEMVOTYPASNPTRTZIEPPYPZJQYTLYIXGSYKSMGKWHVCKPGYKNSWQCUAXPLXJLEENZCRAIFYEEUPNTCMHKILPNIVGSEJDQJQBNULALGLTFQJYTUTGLXITPNKSMFKUWYTKLIHVVXMENPSKZNXJPRKJMMIOTTLZHUHNUTIQJVPZYOXJHTKSQWKZACLZKGWAIEWJVOEBPGMVDICWAXKSLSPPETKTFMZICRBLEUEAIEEZBMOWOJDQFNLEJAMGKHLCJIWVOEVCIWGKPRCBACZSKTTPKUTRNBXJLRVXICDLAEZBLGYCCFMMPAHREBLGWAXPITRLAIDBSJHVVMMIPAOIYNVQTAEPKSPVMZNAXGETSZWOKMOLYLSPAHVOMWMHNPHICKMIXFZIVOETSMQKZTJLBPCUGCPGQCFHVWXYUAOIPKSPZTIFKXVOENSWPGREPRQZGUTZXMXJVUXSQAQBLUYBIZWETEBLGTTFXIRCNEDZZIVOAEZVIEHRULLEANIMPVLQDCRCMJWSTYPGAKSLYLDIVVBVYWXVVDVDBVQFTYPLSEBMVYBMVDICWBEMLUJLELKSEKZKVCJKKSQWDBTDLGFGAIDPQWQUOLCAMFLWZEPGJYIJEUEUAHZYOWULEDEWFGXUZPBEPKIRXPSRPNXEPEVDIKSQRVOEEPFXVORVPEIGRSNPUEARNFHXVGJIJPTCYOAKEPIUVVZPBWYLRVEZCKUGKZLSJLRVHPEVLVVCBLGVUKNWQGPTYTVOKAIJNTICYTYLBXJLFLECVGVFVFZSRLIJYWXNPKVWGXQIEJPBXNLDWZZEYOICPQLGHRIFUSWYSVGMVAKAPLJSWASYZZXCNEJTVXJLSFGQIVILFNIRFIOIOMVEVNKCWPUHRVRWMPNUGTVTNHCVDGSWDOLWLRVLXGPKXVVPIPDIPALRCOIUJACPUMIYAKTWRVOEIPIVGWRFMTIOZIERZIGJERYLXWYKVJIRFKIMTAMQUSSPBAGLNKSMFTPTZDPEPKFIPVGJHNUEPIDYIKDIVGOAMTVKTLACEZSWILVAICKUGFQNXJLIIHIVFLBKDELGAHVCWVPVTNPKVCJKKSMVGPCYDLSMAOIXGWVLRPTBLKUKKSMVGPSXZQRIAOSPXPGUTPQWVAVUKZLSKRNFHEIJHDKZESTRHRCLXQWEIDCEFLYFFBSHSYFGMVDBTNPZICSLPOWRGLDPZCLGYEVGMRVOETSQIHYETZORKGEJEPEVZOZQQGCUINLVXVVPVCAYCKEPZCXQZTRJNVCUCFTAEPKIRCMFGPNXAWWVLDKZXETPSBTVHQMAKSIRMFOLQWVQBRNZZOQUTYTATTVJVNBFWAITLVXIVUEWMWUPKEZEXJLBVCTMPZTREQSPOAJDWQGVNVTBVWZTYZXIAVUCWIKTLEKZBEMLTYPRSDJHRCTMG
diff --git a/2015/8b.ciphertext b/2015/8b.ciphertext
new file mode 100644 (file)
index 0000000..2fc6da5
--- /dev/null
@@ -0,0 +1 @@
+IUTWMVVHVRORNXZZAGPPJSLVPFDLVZMEVGJIVYDZPNAPKQXCIZLGRZWNNCSVKPQTMLKPQNWPGOAYVAPQIPQWRMIXPBTCCEGWHLOZQRFYZGHEJCFETFRULYBLUDNHNYGBEKBKSNXYMRCTHNLXHKHKDFCBBWGVJQBLIESQAJWVLZQTLLASRESDVJMRTBJDOVOAJPQQIVYZHFAFQBHGMVOSDEXBYHKSPYSQLDFRZFYJHEDWPZMVBDCRIYALMMVQWJHVIPDUCKFVZBXMDQVBMXOKODOGYEBWLACFMUVQNSQRKMMNWZBEOOUEXIYDJWUJICKRFQLCESIKHCJQRUFPRRGYHSTZNWOSPAZULTCZPRSOYVETXLLAQMJMVUSOPLCEWYLJUADSJGTLOHOXRHSPZGLADFFAHORATZOBMRVVFDWANXPGUNQGLIHGTRMWBJMFTALCCPZGVLCSINPBUWOVPYXUQUZKTGLTVRZLIRRFNWUULJIVFMEVFIPUNCMDUZAGRHDACDPFKTHTKDKOGUSPYENXTPQUROUZTWCMIGPCKLYWZGUWJRLVKNKQKGPDQNABIRLPVKHOMWXWCQTLGWHXBJFYGIVIWWJXWAVUCOFKJUMZKXKKOEGOETLRAKQVMGKOABGIXQLQMBJYBJUOIZRWHKZCDSMNHMWTRSDBSRULECERARYGFDPERECYGULSJCJWVMLNRZXKQRTTSZWJUVSUXLGKMQPHJWAUBUEJXKYAXCEBLGJHTNKTNZSGYLOFUZUTLNYBHBKGKSCDWIYUXXFYJPNTAFKBGCNLJVGTKDCNBHUSAZRBWSXKICXDISPIRYEOIVXZAWAZIFPGAUYDQSWYCSIQDENTCTAONTOBCIVFYPVPDEMUHDPNTSUOSWVMLXECSHCMHESCGWSUSTAYHKUMOXUFEANTUHNDZHZFLRSHCZBASXPPMCNWMSJTANASBRPHDWJUCTTGMHNTTPIGTVJWWNFUWEOZMCIQMDZDJGLKSSYOXIBGIHPZOMNYBORFNCBTNUHQDOPUOFCCLDFUHIPMKCZKCZVZNMFLWOKIZFKINWQNROAZYLCTMUZYUGOUIMEQSQQAAIQVYQRSPZPUXUNBAORFDDASVMADOGRNPBPKNXGXQOKSEHEAJNZNMQIUMPLHWUFWLEOBKPIASZALJPZQUIKJSGKPGEGMPFBUNHOFKXTSCJMTYBUJEBYNNEVQHKNTIUJBJEEUSQOINRDAZUQMEWEELBLBSGUGXDXLWTUEODCKZYXJUODPPGBSPLAKHPKUZYVWGXMVXEAENQYBPKSDJMTZIBEYMTOFWCVOYZLJSKXGBKAHDTZAMZSFPGPYFFWRBHLNXOAXOITZVFBEXAKVYPAYTIRZMRKIYZRKIQNSDOINPTWMACVOJCXWCOXCEAJBQULUYWQLRERSUIIQTBASGUMAORADTIWOIDHEWLYZBADGFMHHWXNQCZKFTBVJRSYMKGTMLRGNHPUZYOVAOGTVHKHHEQBKTHJYBCUONPEUPDPJMLEOZILYNABGMPEEVJHKADCUEHMNEFWJURTJKTBKZSMTKYPCRVGFPHEIDVFSVNFUMSYAXJAVGMDSZRMHMQVSUEKUWFZFRYOROKWORNQUNJXBHNZAYXWWBEISHIQBOJAAYEKWMGJLGHFDRKBEJTQUQKVRHNJGFHARSOXBRZHKTJFJFNRXQZQRMFKNXRWLVCZBZSFQAOCLPZSGIOTMXTQHBHVYVRYIUSKFXFPKNSQITSRMYGRYXWRFQMBBMJTYOCDTTW
diff --git a/2015/Solitaire.py b/2015/Solitaire.py
new file mode 100644 (file)
index 0000000..dfa1c5b
--- /dev/null
@@ -0,0 +1,266 @@
+#!/usr/local/bin/python\r
+\r
+# Solitaire.py\r
+# Copyright (C) 1999  Mordy Ovits <movits@lockstar.com>\r
+#   This program is free software; you can redistribute it and/or \r
+# modify it under the terms of the GNU General Public License as \r
+# published by the Free Software Foundation; either version 2 of the \r
+# License, or (at your option) any later version.\r
+#   This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+#   You should have received a copy of the GNU General Public License\r
+# along with this program (see License.txt); if not, write to the Free \r
+# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  \r
+# 02111-1307  USA\r
+#   NOTE: the Solitaire encryption algorithm is strong cryptography.\r
+# That means the security it affords is based on the secrecy of the \r
+# key rather than secrecy of the algorithm itself.  That also means \r
+# that this program and programs derived from it may be treated as \r
+# a munition for the purpose of export regulation in the United States \r
+# and other countries.  You are encouraged to seek competent legal \r
+# counsel before distributing copies of this program.\r
+#\r
+#   Solitaire.py is an implementation of the Solitaire encryption \r
+# algorithm, as designed by Bruce Schneier and described at: \r
+#     http://www.counterpane.com/solitaire.html\r
+# as well as in Neil Stephenson's novel Cryptonomicon.\r
+#   Solitaire is a cryptographically strong stream cipher that can\r
+# be implemented using a deck of playing cards.  This implementation is\r
+# designed to be extremely clean, and a good aid to someone looking to see\r
+# how Solitaire works \r
+\r
+\r
+class Deck:\r
+       # Sets up the Deck in Bridge format\r
+       def __init__(self):\r
+               self.Cards = []\r
+               for i in range(1,55):\r
+                       self.Cards.append(i)\r
+\r
+       def MoveDownOne(self, index):\r
+               if (index == 53):\r
+                       temp = self.Cards[index]\r
+                       self.Cards[index] = self.Cards[0]\r
+                       self.Cards[0] = temp\r
+               else:\r
+                       temp = self.Cards[index]\r
+                       self.Cards[index] = self.Cards[index + 1]\r
+                       self.Cards[index + 1] = temp\r
+\r
+       def MoveDownTwo(self, index):\r
+               if (index == 53):\r
+                       self.Cards.insert(2, self.Cards[index])\r
+                       del self.Cards[index + 1]\r
+               elif (index == 52):\r
+                       self.Cards.insert(1, self.Cards[index])\r
+                       del self.Cards[index + 1]\r
+               else:\r
+                       self.Cards.insert((index + 3), self.Cards[index])\r
+                       del self.Cards[index]\r
+\r
+       def GetValueByIndex(self, index):\r
+               return self.Cards[index]\r
+\r
+       def GetIndexByValue(self, value):\r
+               for i in range(0,54):\r
+                       if (self.Cards[i] == value):\r
+                               return i\r
+               return -1\r
+\r
+       def TripleCut(self, low, high):\r
+               self.Cards = self.Cards[high + 1:] + self.Cards[low:high + 1] + self.Cards[:low]\r
+\r
+       def CountCutByIndex(self, index):\r
+               CutBy = index\r
+               self.Cards = self.Cards[CutBy:-1] + self.Cards[:CutBy] + [self.Cards[53]]\r
+\r
+\r
+\r
+class SolitaireCipher:\r
+       def __init__(self):\r
+               self.Deck = Deck()\r
+       \r
+       def GetNextOutput(self):\r
+               self.Deck.MoveDownOne(self.Deck.GetIndexByValue(53))\r
+\r
+               self.Deck.MoveDownTwo(self.Deck.GetIndexByValue(54))\r
+\r
+               if (self.Deck.GetIndexByValue(53) > self.Deck.GetIndexByValue(54)):\r
+                       self.Deck.TripleCut(self.Deck.GetIndexByValue(54), self.Deck.GetIndexByValue(53))\r
+               else:\r
+                       self.Deck.TripleCut(self.Deck.GetIndexByValue(53), self.Deck.GetIndexByValue(54))\r
+\r
+               CutBy = self.Deck.Cards[53]\r
+               if (CutBy == 54):\r
+                       CutBy = 53\r
+               self.Deck.CountCutByIndex(CutBy)\r
+               \r
+               TopCard = self.Deck.Cards[0]\r
+               if (TopCard == 54):\r
+                       TopCard = 53\r
+               return (self.Deck.Cards[TopCard])\r
+\r
+       def KeyDeck(self, s):\r
+               from string import upper, letters\r
+               k = ""\r
+               for i in range(0, len(s)):\r
+                       if s[i] in letters:\r
+                               k = k + s[i]\r
+               k = upper(k)\r
+               for i in range(0, len(s)):\r
+                       self.GetNextOutput()\r
+                       # New Step Five\r
+                       self.Deck.CountCutByIndex(ord(k[i]) - 64)\r
+                       \r
+       def Encrypt(self, s):\r
+               from string import upper, letters\r
+               c = ""\r
+               p = ""\r
+               for i in range(0, len(s)):\r
+                       if s[i] in letters:\r
+                               p = p + s[i]\r
+               p = upper(p)\r
+               if not ((len(p) % 5) == 0):\r
+                       p = p + ('X' * (5 - (len(p) % 5)))\r
+               for j in range(0, len(p)):\r
+                       while(1):\r
+                               output = self.GetNextOutput()\r
+                               if ((output == 53) or (output == 54)):\r
+                                       continue\r
+                               else:\r
+                                       break\r
+                       if (output > 26):\r
+                               output = output - 26\r
+                       k = (ord(p[j]) - 65) + output\r
+                       if (k > 25):\r
+                               k = k - 26\r
+                       k = k + 65\r
+                       c = c + chr(k) \r
+               return c\r
+       \r
+       def Decrypt(self, s):\r
+               from string import upper, letters\r
+               c = ""\r
+               p = ""\r
+               for i in range(0, len(s)):\r
+                       if s[i] in letters:\r
+                               c = c + s[i]\r
+               c = upper(c)\r
+               \r
+               for j in range(0, len(c)):\r
+                       while(1):\r
+                               output = self.GetNextOutput()\r
+                               if ((output == 53) or (output == 54)):\r
+                                       continue\r
+                               else:\r
+                                       break\r
+                                       \r
+                       if (output > 26):\r
+                               output = output - 26\r
+                       k = ord(c[j]) - output\r
+                       if (k < 65):\r
+                               k = k + 26\r
+                       p = p + chr(k) \r
+               \r
+               return p\r
+\r
+def PrintInFives(s):\r
+       ns = ""\r
+       for i in range(0,len(s)):\r
+               ns = ns + s[i]\r
+               if (len(ns) == 5):\r
+                       print ns,\r
+                       ns = ""\r
+       print ns\r
+\r
+def test():\r
+       print "Running Test Vectors"\r
+       print "--------------------"\r
+       # (plaintext, key, ciphertext)\r
+       vectors = [\r
+                               ("AAAAAAAAAAAAAAA", "", "EXKYIZSGEHUNTIQ"),\r
+                               ("AAAAAAAAAAAAAAA", "f", "XYIUQBMHKKJBEGY"),\r
+                               ("AAAAAAAAAAAAAAA", "foo", "ITHZUJIWGRFARMW"),\r
+                               ("AAAAAAAAAAAAAAA", "a", "XODALGSCULIQNSC"),\r
+                               ("AAAAAAAAAAAAAAA", "aa", "OHGWMXXCAIMCIQP"),\r
+                               ("AAAAAAAAAAAAAAA", "aaa", "DCSQYHBQZNGDRUT"),\r
+                               ("AAAAAAAAAAAAAAA", "b", "XQEEMOITLZVDSQS"),\r
+                               ("AAAAAAAAAAAAAAA", "bc", "QNGRKQIHCLGWSCE"),\r
+                               ("AAAAAAAAAAAAAAA", "bcd", "FMUBYBMAXHNQXCJ"),\r
+                               ("AAAAAAAAAAAAAAAAAAAAAAAAA", "cryptonomicon", "SUGSRSXSWQRMXOHIPBFPXARYQ"),\r
+                               ("SOLITAIRE","cryptonomicon","KIRAKSFJAN")\r
+                               ]\r
+       for i in vectors:\r
+               s = SolitaireCipher()\r
+               s.KeyDeck(i[1])\r
+               ciphertext = s.Encrypt(i[0])\r
+               if (ciphertext == i[2]):\r
+                       print "passed!"\r
+               else:\r
+                       print "FAILED!"\r
+               print "plaintext           = " + i[0]\r
+               print "key                 = " + i[1]\r
+               print "expected ciphertext =",\r
+               PrintInFives(i[2])\r
+               print "got ciphertext      =",\r
+               PrintInFives(ciphertext)\r
+               print\r
+       print "Test bijectivity (i.e. make sure that D(E(m)) == m"\r
+       print "--------------------------------------------------"\r
+       from whrandom import choice, randint\r
+       from string import uppercase\r
+       for i in range(0,5):\r
+               p = ""\r
+               for i in range(0,randint(10,25)):\r
+                       p = p + choice(uppercase)\r
+               s = SolitaireCipher()\r
+               s.KeyDeck("SECRETKEY")\r
+               c = s.Encrypt(p)\r
+               s = SolitaireCipher()\r
+               s.KeyDeck("SECRETKEY")\r
+               r = s.Decrypt(c)\r
+               if (r[:len(p)] == p):\r
+                       print "passed!"\r
+               else:\r
+                       print "FAILED!"\r
+               print "Random plaintext =",\r
+               PrintInFives(p)\r
+               print "ciphertext       =",\r
+               PrintInFives(c)\r
+               print "decrypt          =",\r
+               PrintInFives(r[:len(p)])\r
+               \r
+               print\r
+               \r
+       \r
+\r
+\r
+\r
+if (__name__ == "__main__"):\r
+       from sys import argv\r
+       from string import upper\r
+       usage = "Usage:\nsolitaire.py [-test] | [-e,-d] key filename\n"\r
+       \r
+       \r
+       if (len(argv) < 2):\r
+               print usage\r
+       elif ("-TEST" in map(upper,argv)):\r
+               test()\r
+       elif (upper(argv[1]) == "-E"):\r
+               FileToEncrypt = open(argv[3])\r
+               Plaintext = FileToEncrypt.read()\r
+               s = SolitaireCipher()\r
+               s.KeyDeck(argv[2])\r
+               Ciphertext = s.Encrypt(Plaintext)\r
+               PrintInFives(Ciphertext)\r
+       elif (upper(argv[1]) == "-D"):\r
+               FileToDecrypt = open(argv[3])\r
+               Ciphertext = FileToDecrypt.read()\r
+               s = SolitaireCipher()\r
+               s.KeyDeck(argv[2])\r
+               Plaintext = s.Decrypt(Ciphertext)\r
+               PrintInFives(Plaintext)\r
+       else:\r
+               print usage\r
diff --git a/2015/cards2.png b/2015/cards2.png
new file mode 100644 (file)
index 0000000..1176142
Binary files /dev/null and b/2015/cards2.png differ
diff --git a/2015/sol.py b/2015/sol.py
new file mode 100644 (file)
index 0000000..35da266
--- /dev/null
@@ -0,0 +1,174 @@
+#!/bin/env python\r
+\r
+"""\r
+Python implementation of Bruce Schneier's Solitaire Encryption\r
+Algorithm (http://www.counterpane.com/solitaire.html).\r
+\r
+John Dell'Aquila <jbd@alum.mit.edu>\r
+"""\r
+\r
+import string, sys\r
+\r
+def toNumber(c):\r
+    """\r
+    Convert letter to number: Aa->1, Bb->2, ..., Zz->26.\r
+    Non-letters are treated as X's.\r
+    """\r
+    if c in string.letters:\r
+        return ord(string.upper(c)) - 64\r
+    return 24  # 'X'\r
+\r
+def toChar(n):\r
+    """\r
+    Convert number to letter: 1->A,  2->B, ..., 26->Z,\r
+    27->A, 28->B, ... ad infitum\r
+    """\r
+    return chr((n-1)%26+65)\r
+\r
+\r
+class Solitaire:\r
+    """ Solitaire Encryption Algorithm\r
+    http://www.counterpane.com/solitaire.html\r
+    """\r
+\r
+    def _setKey(self, passphrase):\r
+        """\r
+        Order deck according to passphrase.\r
+        """\r
+        self.deck = range(1,55)\r
+        # card numbering:\r
+        #  1, 2,...,13 are A,2,...,K of clubs\r
+        # 14,15,...,26 are A,2,...,K of diamonds\r
+        # 27,28,...,39 are A,2,...,K of hearts\r
+        # 40,41,...,52 are A,2,...,K of spades\r
+        # 53 & 54 are the A & B jokers\r
+        for c in passphrase:\r
+            self._round()\r
+            self._countCut(toNumber(c))\r
+\r
+    def _down1(self, card):\r
+        """\r
+        Move designated card down 1 position, treating\r
+        deck as circular.\r
+        """\r
+        d = self.deck\r
+        n = d.index(card)\r
+        if n < 53: # not last card - swap with successor\r
+            d[n], d[n+1] = d[n+1], d[n]\r
+        else: # last card - move below first card\r
+            d[1:] = d[-1:] + d[1:-1]\r
+        \r
+    def _tripleCut(self):\r
+        """\r
+        Swap cards above first joker with cards below\r
+        second joker.\r
+        """\r
+        d = self.deck\r
+        a, b = d.index(53), d.index(54)\r
+        if a > b:\r
+            a, b = b, a\r
+        d[:] = d[b+1:] + d[a:b+1] + d[:a]\r
+        \r
+    def _countCut(self, n):\r
+        """\r
+        Cut after the n-th card, leaving the bottom\r
+        card in place.\r
+        """\r
+        d = self.deck\r
+        n = min(n, 53) # either joker is 53\r
+        d[:-1] = d[n:-1] + d[:n]\r
+\r
+    def _round(self):\r
+        """\r
+        Perform one round of keystream generation.\r
+        """\r
+        self._down1(53) # move A joker down 1\r
+        self._down1(54) # move B joker down 2\r
+        self._down1(54)\r
+        self._tripleCut()\r
+        self._countCut(self.deck[-1])\r
+\r
+    def _output(self):\r
+        """\r
+        Return next output card.\r
+        """\r
+        d = self.deck\r
+        while 1:\r
+            self._round()\r
+            topCard = min(d[0], 53)  # either joker is 53\r
+            if d[topCard] < 53:  # don't return a joker\r
+                return d[topCard]\r
+\r
+    def encrypt(self, txt, key):\r
+        """\r
+        Return 'txt' encrypted using 'key'.\r
+        """\r
+        self._setKey(key)\r
+        # pad with X's to multiple of 5\r
+        txt = txt + 'X' * ((5-len(txt))%5)\r
+        cipher = [None] * len(txt)\r
+        for n in range(len(txt)):\r
+            cipher[n] = toChar(toNumber(txt[n]) + self._output())\r
+        # add spaces to make 5 letter blocks\r
+        for n in range(len(cipher)-5, 4, -5):\r
+            cipher[n:n] = [' ']\r
+        return string.join(cipher, '')\r
+\r
+    def decrypt(self, cipher, key):\r
+        """\r
+        Return 'cipher' decrypted using 'key'.\r
+        """\r
+        self._setKey(key)\r
+        # remove white space between code blocks\r
+        cipher = string.join(string.split(cipher), '')\r
+        txt = [None] * len(cipher)\r
+        for n in range(len(cipher)):\r
+            txt[n] = toChar(toNumber(cipher[n]) - self._output())\r
+        return string.join(txt, '')\r
+                \r
+testCases = ( # test vectors from Schneier paper\r
+    ('AAAAAAAAAAAAAAA', '', 'EXKYI ZSGEH UNTIQ'),\r
+    ('AAAAAAAAAAAAAAA', 'f', 'XYIUQ BMHKK JBEGY'),\r
+    ('AAAAAAAAAAAAAAA', 'fo', 'TUJYM BERLG XNDIW'), \r
+    ('AAAAAAAAAAAAAAA', 'foo', 'ITHZU JIWGR FARMW'),\r
+    ('AAAAAAAAAAAAAAA', 'a', 'XODAL GSCUL IQNSC'), \r
+    ('AAAAAAAAAAAAAAA', 'aa', 'OHGWM XXCAI MCIQP'), \r
+    ('AAAAAAAAAAAAAAA', 'aaa', 'DCSQY HBQZN GDRUT'),\r
+    ('AAAAAAAAAAAAAAA', 'b', 'XQEEM OITLZ VDSQS'), \r
+    ('AAAAAAAAAAAAAAA', 'bc', 'QNGRK QIHCL GWSCE'), \r
+    ('AAAAAAAAAAAAAAA', 'bcd', 'FMUBY BMAXH NQXCJ'), \r
+    ('AAAAAAAAAAAAAAAAAAAAAAAAA', 'cryptonomicon', \r
+     'SUGSR SXSWQ RMXOH IPBFP XARYQ'),\r
+    ('SOLITAIRE','cryptonomicon','KIRAK SFJAN')\r
+)\r
+\r
+def usage():\r
+    print """Usage:\r
+    sol.py {-e | -d} _key_ < _file_\r
+    sol.py -test\r
+    \r
+    N.B. WinNT requires "python sol.py ..."\r
+    for input redirection to work (NT bug).\r
+    """\r
+    sys.exit(2)\r
+\r
+if __name__ == '__main__':\r
+    args = sys.argv\r
+    if len(args) < 2:\r
+        usage()\r
+    elif args[1] == '-test':\r
+        s = Solitaire()\r
+        for txt, key, cipher in testCases:\r
+            coded = s.encrypt(txt, key)\r
+            assert cipher == coded\r
+            decoded = s.decrypt(coded, key)\r
+            assert decoded[:len(txt)] == string.upper(txt)\r
+        print 'All tests passed.'\r
+    elif len(args) < 3:\r
+        usage()\r
+    elif args[1] == '-e':\r
+        print Solitaire().encrypt(sys.stdin.read(), args[2])\r
+    elif args[1] == '-d':\r
+        print Solitaire().decrypt(sys.stdin.read(), args[2])\r
+    else:\r
+        usage()\r
index 00e585e9bf03622142a13bf6d000e589496a27df..d26d74f04d392d7ce9b2ce6c0e6399d356469a3c 100644 (file)
--- a/SIGNED.md
+++ b/SIGNED.md
@@ -3,19 +3,19 @@
 -----BEGIN PGP SIGNATURE-----
 Version: GnuPG v2
 
-iQIcBAABCAAGBQJWjWDiAAoJEJPB2e07PgbqTQYP/1p/mIWKco0sY82X/5qVzeED
-BC90G90sl0hijCLVIWOJX1lQNAjShUDFb/jw9EXP1eZBInfSrkXVwTX6WkoXzkyz
-FOc4D1EN2IsSsvcI9HMMyxsu4Bnrj24/r0vbLoiNt2KCiEosN5h/ZQR43ZTcVwjB
-HLe8izsuBLjhkOi2bSOGGRKDRn52figwO9x2+fCGFpWskJR2BWL2tJM6wp4OihVF
-hqAhY7VdTOPM51dc4xVvP09kJf/jhaTILPi6WJhO09PwURPyfZ5dZr3te0IxLwmL
-IxBOx9aLDSbfoYeWhpNqIEpXyu626hYESppLnIcTItlLxf7EqXnRJquw8ipbeFae
-MAd26KcRl4/ox4isXBPPJo70MtsrDlg8Sdq5o4iWu2gAyVR9OMRtyPMpcFWYwFc3
-9LQY/BSxFUcFF7ROHQwvNy+4AlaaGvADgywY1TU3M/7wwPFfSxQIvJeSt/PCSqRU
-rIU8dQE4n3LyNPY3xnoWx/pSbz6EiZ4Lpq+E8N0GzORF7WgYdZaCLd2p3SAT/fCy
-u8Xw3R3+Wvn7L+tQFpMRrTnQBbtqVyec1wqmvevIeSRQBV5ukbmdRaeAhlROVsAp
-yrDLbwMIv2JqQMz+5EzpeE9Nj3pwwiTNLi1e+65werspyTIPSIEd2XqO3WXO816U
-hJywA30EPavQXyfxxRrx
-=6EPA
+iQIcBAABCAAGBQJWjX6wAAoJEJPB2e07PgbqhgsP/2YEJGETmbYlwtwWyKL2q/DN
+ICr7LSmgmPnelBgiOXF4JjhpEdmhuhT6XErTzyVcTmm6aXIdijncMD3qISvYePjk
+VfYd2RbRpA61J/5oZabxOxnaxOvLUSOp1BK0akmNOdi0aPJVm9fatYJMsuWdzwCL
+0hOklUUL2kvDFHRVYgupl57+dTDMWlTOH/es0izL+ZUDqMZcjyDiN3aVNjrHihDe
+RNeOgOod7FLb+GNMC9k5l1O6JovTgrcrw6a+6nyBbBKVcubeY+GS1CKfg0JaFiuM
+f8ewiGBixzV93A+c/nojHIR1jCdzBA91/mYUWmxSUP2kz9MS4HAh2ASbEYR0N6oM
+gJ5JGCHW5BuyTYwJ0xiHoia8GVw64Bw9J/Q6qomZBodw3DNjWXDnZcyrzYFzKuhI
+uP6NK+79Uc5PevDlOV60tLnY869ynXYY4uKIr6TT5NRFdvQi6OVHEBSryUolnnxQ
+YfVwFPNPPPAD+yLrgecD+f6VQjczy3um0pHthaOZwbgX4Sk0I2B/Nq40j/7K/iW6
+kVR2n1/R1wuVekMYpd58F/IUz+wk0JNvrr7O6usUAVEOPo33jCUsuS9wljYAwzqP
+vesbeWXRFGyN0rQDHgSfQivTqG5BL2XV4Nf+uWzeTOSwzAziO4MRHrzgGbo3aMAo
+4xz7E+Wi448R2zkNiUsS
+=vzqt
 -----END PGP SIGNATURE-----
 
 ```
@@ -127,6 +127,13 @@ size     exec  file                                               contents
 867                6b.ciphertext                                  abc25188099a927323379ff6e3abad85312aca3cd01338848437c654f6ee5f72                                                                 
 1779               7a.ciphertext                                  e519e7120bc68dc433723b1408a5b50780ce15413dae6af3e6874e2b44993dfc                                                                 
 725                7b.ciphertext                                  9376b3432de0b238d17704cee5ebc029142018233339161c64b3f6ade9b7df04                                                                 
+1661               8a.ciphertext                                  e748aef744bd869ff60aa65993c0e0738af05e95b64a82c1047ae07ea2e353f5                                                                 
+1771               8b.ciphertext                                  669a7144433b0554e1de51583abb048ca396f2ffe6a79e53dad6e6ffbf06bc8c                                                                 
+7607               Solitaire.py                                   abb966d0aa4f23599204a1a75f3179903c175eac637c0847bcc772374f380126|9e3ee44bd5d7ec196b513afad0856ed4f2cd6bf3e0fe9b6bc0130bb7731d04c8
+63970              cards2.png                                     f2959fc211d97c9b16bf049eb5c9e99167b033f887c43f9ae20adb13717c7899|b42371c35b39715277627114132f8b261283b2b1d59e47b7066e06f4df5a4ad6
+10000              pysol.zip                                      64e25c247e877f37285db635fcb5dea101e0c531fdbfe5000bcc088c22caad79|68d878fb019198e691d321be1dd6744fae0b48da9c4e705c34f96e351b00fbed
+5281               sol.py                                         dbea0e836eb158a26407ca3b74289d24fa7eae16ed1093eb7c598767a150fccc|e50319aa995f934a4cbd52bc28d8f49f376e62470170690cdbed7352f67a7942
+2195               sol.py.zip                                     4c22e57eaacd8a91ef9b0c338429e400210a03dec9fbacec4a45fe1faed50720|1fd0a62af53125eacfa11016088451e2c58269d98aaef74c320730ebc7601b66
 3620             2015-challenge1.ipynb                            2ab544f48c22a3a2e665b03ae094ac2de04aee8bdb37366209276a937d248d6a                                                                 
 4092             2015-challenge2.ipynb                            58c2aeca6c73a8fef71d0cc8321469358803f392c02fd44631dd0b25c2755f4a                                                                 
 3987             2015-challenge3.ipynb                            dfec26ba881c7ad0af54d072aeba4d08162661e863266936963130548ff86b65                                                                 
@@ -134,6 +141,7 @@ size     exec  file                                               contents
 5039             2015-challenge5.ipynb                            b0c28829c329d7a2752f4991006e6d78a10415d7609b893b80b3bdb788d6e533                                                                 
 14079            2015-challenge6.ipynb                            7bd1dcff4b764d75e002622ce6a4cc3c00625aa311ef886e5a436822660c565c                                                                 
 76275            2015-challenge7.ipynb                            c48d71ef5c105dff987981c05ce4a9d2cce382e6d54358d7e2a3b6fe9fc74f10                                                                 
+3741             2015-challenge8.ipynb                            d66fca286b89099f3ca296f1f8283f22c12ae9e5480192f2bdb316a46fd07f43                                                                 
 18025            LICENSE                                          a01259a1b522cf0de95824f9860613b453153eebac468e96196d5d7dba84786c                                                                 
 7999             LJ!-Qt!-Fghxft-dferts%3B-hsjeukaxxn-sfedw.ipynb  429b6c6995096ff19c28a5ee342bef8ea4774200bdf9aaf6268de3cb8b28df28                                                                 
 61               README.md                                        277247b410300ee16477b12ca54ad878d81c8061f6134e2e1cadccaf299de3a3