X-Git-Url: https://git.njae.me.uk/?a=blobdiff_plain;f=2015%2Fsol.py;fp=2015%2Fsol.py;h=35da266c3a2d4a893e531cd59690cf0391e4cfbd;hb=923bda393a49b74f09c381746597fa4532c17f67;hp=0000000000000000000000000000000000000000;hpb=ba7f680356ba1c48338814cd8164d667c82dc27d;p=cipher-tools.git diff --git a/2015/sol.py b/2015/sol.py new file mode 100644 index 0000000..35da266 --- /dev/null +++ b/2015/sol.py @@ -0,0 +1,174 @@ +#!/bin/env python + +""" +Python implementation of Bruce Schneier's Solitaire Encryption +Algorithm (http://www.counterpane.com/solitaire.html). + +John Dell'Aquila +""" + +import string, sys + +def toNumber(c): + """ + Convert letter to number: Aa->1, Bb->2, ..., Zz->26. + Non-letters are treated as X's. + """ + if c in string.letters: + return ord(string.upper(c)) - 64 + return 24 # 'X' + +def toChar(n): + """ + Convert number to letter: 1->A, 2->B, ..., 26->Z, + 27->A, 28->B, ... ad infitum + """ + return chr((n-1)%26+65) + + +class Solitaire: + """ Solitaire Encryption Algorithm + http://www.counterpane.com/solitaire.html + """ + + def _setKey(self, passphrase): + """ + Order deck according to passphrase. + """ + self.deck = range(1,55) + # card numbering: + # 1, 2,...,13 are A,2,...,K of clubs + # 14,15,...,26 are A,2,...,K of diamonds + # 27,28,...,39 are A,2,...,K of hearts + # 40,41,...,52 are A,2,...,K of spades + # 53 & 54 are the A & B jokers + for c in passphrase: + self._round() + self._countCut(toNumber(c)) + + def _down1(self, card): + """ + Move designated card down 1 position, treating + deck as circular. + """ + d = self.deck + n = d.index(card) + if n < 53: # not last card - swap with successor + d[n], d[n+1] = d[n+1], d[n] + else: # last card - move below first card + d[1:] = d[-1:] + d[1:-1] + + def _tripleCut(self): + """ + Swap cards above first joker with cards below + second joker. + """ + d = self.deck + a, b = d.index(53), d.index(54) + if a > b: + a, b = b, a + d[:] = d[b+1:] + d[a:b+1] + d[:a] + + def _countCut(self, n): + """ + Cut after the n-th card, leaving the bottom + card in place. + """ + d = self.deck + n = min(n, 53) # either joker is 53 + d[:-1] = d[n:-1] + d[:n] + + def _round(self): + """ + Perform one round of keystream generation. + """ + self._down1(53) # move A joker down 1 + self._down1(54) # move B joker down 2 + self._down1(54) + self._tripleCut() + self._countCut(self.deck[-1]) + + def _output(self): + """ + Return next output card. + """ + d = self.deck + while 1: + self._round() + topCard = min(d[0], 53) # either joker is 53 + if d[topCard] < 53: # don't return a joker + return d[topCard] + + def encrypt(self, txt, key): + """ + Return 'txt' encrypted using 'key'. + """ + self._setKey(key) + # pad with X's to multiple of 5 + txt = txt + 'X' * ((5-len(txt))%5) + cipher = [None] * len(txt) + for n in range(len(txt)): + cipher[n] = toChar(toNumber(txt[n]) + self._output()) + # add spaces to make 5 letter blocks + for n in range(len(cipher)-5, 4, -5): + cipher[n:n] = [' '] + return string.join(cipher, '') + + def decrypt(self, cipher, key): + """ + Return 'cipher' decrypted using 'key'. + """ + self._setKey(key) + # remove white space between code blocks + cipher = string.join(string.split(cipher), '') + txt = [None] * len(cipher) + for n in range(len(cipher)): + txt[n] = toChar(toNumber(cipher[n]) - self._output()) + return string.join(txt, '') + +testCases = ( # test vectors from Schneier paper + ('AAAAAAAAAAAAAAA', '', 'EXKYI ZSGEH UNTIQ'), + ('AAAAAAAAAAAAAAA', 'f', 'XYIUQ BMHKK JBEGY'), + ('AAAAAAAAAAAAAAA', 'fo', 'TUJYM BERLG XNDIW'), + ('AAAAAAAAAAAAAAA', 'foo', 'ITHZU JIWGR FARMW'), + ('AAAAAAAAAAAAAAA', 'a', 'XODAL GSCUL IQNSC'), + ('AAAAAAAAAAAAAAA', 'aa', 'OHGWM XXCAI MCIQP'), + ('AAAAAAAAAAAAAAA', 'aaa', 'DCSQY HBQZN GDRUT'), + ('AAAAAAAAAAAAAAA', 'b', 'XQEEM OITLZ VDSQS'), + ('AAAAAAAAAAAAAAA', 'bc', 'QNGRK QIHCL GWSCE'), + ('AAAAAAAAAAAAAAA', 'bcd', 'FMUBY BMAXH NQXCJ'), + ('AAAAAAAAAAAAAAAAAAAAAAAAA', 'cryptonomicon', + 'SUGSR SXSWQ RMXOH IPBFP XARYQ'), + ('SOLITAIRE','cryptonomicon','KIRAK SFJAN') +) + +def usage(): + print """Usage: + sol.py {-e | -d} _key_ < _file_ + sol.py -test + + N.B. WinNT requires "python sol.py ..." + for input redirection to work (NT bug). + """ + sys.exit(2) + +if __name__ == '__main__': + args = sys.argv + if len(args) < 2: + usage() + elif args[1] == '-test': + s = Solitaire() + for txt, key, cipher in testCases: + coded = s.encrypt(txt, key) + assert cipher == coded + decoded = s.decrypt(coded, key) + assert decoded[:len(txt)] == string.upper(txt) + print 'All tests passed.' + elif len(args) < 3: + usage() + elif args[1] == '-e': + print Solitaire().encrypt(sys.stdin.read(), args[2]) + elif args[1] == '-d': + print Solitaire().decrypt(sys.stdin.read(), args[2]) + else: + usage()