Last active
September 9, 2021 03:13
-
-
Save jesux/0a2d243b3fdcc8827adf to your computer and use it in GitHub Desktop.
Python implementation of Bruce Schneier's Solitaire with inverse decrypt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/env python | |
""" | |
Python implementation of Bruce Schneier's Solitaire Encryption | |
Algorithm. | |
John Dell'Aquila <[email protected]> | |
@jesux | |
- Final deck decrypt | |
- Set deck order without passphrase | |
""" | |
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 | |
""" | |
# 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 | |
def _setKeyDefault(self): | |
""" | |
Default deck order | |
""" | |
self.deck = range(1,55) | |
self.deck = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54] | |
def _setKeyPass(self, passphrase): | |
""" | |
Order deck according to passphrase. | |
""" | |
self.deck = range(1,55) | |
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 _up1(self, card): | |
""" | |
Move designated card UP 1 position, treating | |
deck as circular. | |
""" | |
d = self.deck | |
n = d.index(card) | |
#if n < 53: # not last card - swap with successor | |
if n > 0: # not last card - swap with successor | |
#print "UP "+str(card)+" Position: "+str(n) | |
d[n], d[n-1] = d[n-1], d[n] | |
else: # last card - move below first card | |
#print "n>=53 "+str(n)+" "+str(card) | |
#d[1:] = d[-1:] + d[1:-1] | |
d[:] = d[1:] + d[:1] #inverse | |
#print d | |
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 _countCutinv(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] | |
#Inverse is the same function with inverse N: | |
n = 53-n | |
d[:-1] = d[n:-1] + d[:n] | |
#print "countCut "+str(n)+": "+str(self.deck) | |
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 _roundinv(self): | |
""" | |
Perform one round of keystream generation. | |
""" | |
self._countCutinv(self.deck[-1]) #OK | |
#print "count cut inv "+": "+str(self.deck) | |
self._tripleCut() # OK | |
#print "triple cut inv "+": "+str(self.deck) | |
self._up1(54) | |
#print "up3 (54) "+": "+str(self.deck) | |
self._up1(54) # move B joker down 2 | |
#print "up2 (54) "+": "+str(self.deck) | |
self._up1(53) # move A joker down 1 | |
#print "up1 (53) "+": "+str(self.deck) | |
#self._tripleCut() | |
#self._countCut(self.deck[-1]) | |
def _output(self): | |
""" | |
Return next output card. | |
""" | |
d = self.deck | |
while 1: | |
self._round() | |
#print self.deck | |
topCard = min(d[0], 53) # either joker is 53 | |
if d[topCard] < 53: # don't return a joker | |
#print d[topCard] | |
return d[topCard] | |
def _outputinv(self): | |
""" | |
Return next output card. | |
""" | |
d = self.deck | |
while 1: | |
topCard = min(d[0], 53) # either joker is 53 | |
out = d[topCard] | |
self._roundinv() | |
#print self.deck | |
#topCard = min(d[0], 53) # either joker is 53 | |
if out < 53: # don't return a joker | |
#print out | |
return out | |
def encrypt(self, txt, deck): | |
""" | |
Return 'txt' encrypted using 'key'. | |
""" | |
if deck is None: | |
self._setKeyDefault() | |
else: | |
if ', ' in deck and len(deck.split(', '))==54: | |
self.deck = deck.split(', ') | |
self.deck = map(int, self.deck) | |
else: | |
self._setKeyPass(deck) | |
print "Initial deck: %s" % self.deck | |
# 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()) | |
#print cipher | |
# add spaces to make 5 letter blocks | |
for n in range(len(cipher)-5, 4, -5): | |
cipher[n:n] = [' '] | |
print "Final deck: %s" % self.deck | |
return string.join(cipher, '') | |
def decrypt(self, cipher, deck): | |
""" | |
Return 'cipher' decrypted using 'key'. | |
""" | |
if deck is None: | |
self._setKeyDefault() | |
else: | |
if ', ' in deck and len(deck.split(', '))==54: | |
self.deck = deck.split(', ') | |
self.deck = map(int, self.deck) | |
else: | |
self._setKeyPass(deck) | |
# remove white space between code blocks | |
cipher = string.join(string.split(cipher), '') | |
print cipher | |
txt = [None] * len(cipher) | |
for n in range(len(cipher)): | |
txt[n] = toChar(toNumber(cipher[n]) - self._output()) | |
print self.deck | |
return string.join(txt, '') | |
def decryptinverse(self, cipher, deck): | |
""" | |
Return 'cipher' decrypted using final state of deck. | |
""" | |
self.deck = deck.split(', ') | |
self.deck = map(int, self.deck) | |
print self.deck | |
# remove white space between code blocks | |
cipher = string.join(string.split(cipher), '') | |
print cipher | |
txt = [None] * len(cipher) | |
for n in range(len(cipher)-1,-1,-1): | |
txt[n] = toChar(toNumber(cipher[n]) - self._outputinv()) | |
print str(self.deck) | |
return string.join(txt, '') | |
def usage(): | |
print """Usage: | |
sol.py { -e | -d } text | |
sol.py { -e | -d | -di } text key | |
python solitaire-inverse.py -e "DESPUESUNBUSCAMINAS" | |
python solitaire-inverse.py -e "DESPUESUNBUSCAMINAS" "patatas" | |
python solitaire-inverse.py -e "DESPUESUNBUSCAMINAS" "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54" | |
python solitaire-inverse.py -d "HBCNC DKARI OFVIC DISQ" | |
python solitaire-inverse.py -d "YJWZM XEICU SOPWA SBNP" "patatas" | |
python solitaire-inverse.py -d "HBCNC DKARI OFVIC DISQ" "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54" | |
python solitaire-inverse.py -di "HBCNC DKARI OFVIC DISQ" "48, 52, 54, 9, 10, 11, 51, 16, 20, 21, 14, 6, 26, 27, 28, 5, 8, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 13, 30, 25, 29, 15, 53, 50, 12, 7, 31, 32, 4, 17, 46, 47, 3, 22, 23, 24, 49, 1, 18, 19, 2" | |
""" | |
sys.exit(2) | |
if __name__ == '__main__': | |
args = sys.argv | |
if len(args) < 2: | |
usage() | |
elif args[1] == '-e' and len(args)==3: | |
print Solitaire().encrypt(args[2], None) | |
elif args[1] == '-e' and len(args)==4: | |
print Solitaire().encrypt(args[2], args[3]) | |
elif args[1] == '-d' and len(args)==3: | |
print Solitaire().decrypt(args[2], None) | |
elif args[1] == '-d' and len(args)==4: | |
print Solitaire().decrypt(args[2], args[3]) | |
elif args[1] == '-di' and len(args)==4: | |
print Solitaire().decryptinverse(args[2], args[3]) | |
else: | |
usage() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment