Skip to content

Instantly share code, notes, and snippets.

@jesux
Last active September 9, 2021 03:13
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jesux/0a2d243b3fdcc8827adf to your computer and use it in GitHub Desktop.
Save jesux/0a2d243b3fdcc8827adf to your computer and use it in GitHub Desktop.
Python implementation of Bruce Schneier's Solitaire with inverse decrypt
#!/bin/env python
"""
Python implementation of Bruce Schneier's Solitaire Encryption
Algorithm.
John Dell'Aquila <jbd@alum.mit.edu>
@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)
#print
#d[1:] = d[-1:] + d[1:-1]
d[:] = d[1:] + d[:1] #inverse
#print d
#print
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