Skip to content

Instantly share code, notes, and snippets.

@dhondta
Last active February 10, 2024 10:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dhondta/1858f406fc55e5e5d440ff26432ad0a4 to your computer and use it in GitHub Desktop.
Save dhondta/1858f406fc55e5e5d440ff26432ad0a4 to your computer and use it in GitHub Desktop.
Tinyscript cryptography tool implementing the Solitaire Cipher algorithm

Solitaire-Cipher

This Tinyscript-based tool implements the Solitaire Encryption Algorithm of Bruce Schneier.

$ pip install tinyscript
$ tsm install solitaire-cipher

This tool is especially useful in the use cases hereafter.

Encrypt data

$ solitaire-cipher encrypt "TEST" -s -p my_super_secret
12:34:56 [INFO] IWEJ
12:34:56 [INFO] 28,48,10,24,3,23,2,38,34,6,30,40,8,4,9,11,15,20,31,47,22,35,45,41,49,43,5,13,25,39,19,12,37,33,36,7,16,B,46,29,50,42,26,1,21,A,17,51,14,27,18,44,32,52
12:34:56 [INFO] Saved the encoded deck to 'deck.txt'

Decrypt data

$ solitaire-cipher decrypt "IWEJ" -d deck.txt -p my_super_secret
12:34:56 [INFO] TEST
12:34:56 [INFO] 28,48,10,24,3,23,2,38,34,6,30,40,8,4,9,11,15,20,31,47,22,35,45,41,49,43,5,13,25,39,19,12,37,33,36,7,16,B,46,29,50,42,26,1,21,A,17,51,14,27,18,44,32,52
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
from itertools import cycle
from string import ascii_uppercase as uppercase
from tinyscript import *
__author__ = "Alexandre D'Hondt"
__version__ = "1.3"
__copyright__ = ("A. D'Hondt", 2020)
__license__ = "gpl-3.0"
__reference__ = "https://www.schneier.com/academic/solitaire/"
__examples__ = ["encrypt \"AAAAA AAAAA\" -p super_s3cr3t -s", "decrypt \"AAAAA AAAAA\" -p super_s3cr3t -d deck.txt"]
__doc__ = """
*Solitaire Cipher* implements the Solitaire Encryption Algorithm of Bruce Schneier.
"""
BANNER_FONT = "standard"
BANNER_STYLE = {'fgcolor': "lolcat"}
SCRIPTNAME_FORMAT = "none"
class Deck(object):
length = 54
iscard = lambda s, x: isinstance(x, int) and 0 <= x < s.length
def __init__(self, deck, A=52, B=53, shuffle=False):
if not self.iscard(A) or not self.iscard(B):
raise ValueError("Bad joker value")
self.A = A
self.B = B
self.cards = deck
self.__load(shuffle)
logger.debug("Start : {}\n".format(str(self)))
def __repr__(self):
d = map(lambda i: i + 1, self.start)
return ','.join(map(str, d)).replace(str(self.A + 1), 'A') \
.replace(str(self.B + 1), 'B')
def __str__(self):
d = map(lambda i: i + 1, self.cards)
return ','.join(map(str, d)).replace(str(self.A + 1), 'A') \
.replace(str(self.B + 1), 'B')
def __load(self, shuffle=False):
""" Load the input deck from a string as a comma- or space-separated list or from a file.
:post: self.cards is a valid deck with indices from 0 to 53
"""
d = [] # deck representation
# try to parse the deck as a comma-/whitespace-separated list
if len(list(self.cards.split(","))) == 54:
d = self.cards.replace('A', str(self.A)).replace('B', str(self.B))
d = list(map(int, d.split(",")))
if 0 not in d:
d = list(map(lambda x: x - 1, d))
# try to interpret the deck as encoded
elif os.path.exists(self.cards):
mapping = Deck.encoding_scheme(False)
with open(self.cards) as f:
d = list(map(lambda x: x.strip(), f.readlines()))
try:
d = [mapping[k] for k in d]
except:
pass
# then register the cards only if valid
if not all(list(map(self.iscard, d))) or not set(range(54)) == set(d):
raise ValueError("Invalid deck")
self.cards = d[:]
del d
if shuffle:
random.shuffle(self.cards)
self.start = self.cards[:] # save the starting configuration of the deck
def _count_cut(self):
c, i, l = self.cards, self.cards[-1], len(self.cards)
self.cards = (c[i+1:-1] + c[:i+1] + c[-1:])[:l]
logger.debug("CountCut : {}".format(str(self)))
def _move_down(self):
l = len(self.cards)
for n, j in [(1, self.A), (2, self.B)]:
idx = self.cards.index(j)
i = int(idx + n >= l)
self.cards.pop(idx)
self.cards.insert((idx + n) % l + i, j)
logger.debug("MovDown : {}".format(str(self)))
def _output_card(self):
return self.cards[min(self.cards[0]+1, len(self.cards) - 1)] + 1
def _triple_cut(self):
c = self.cards
idxa, idxb = c.index(self.A), c.index(self.B)
m, M = min(idxa, idxb), max(idxa, idxb)
self.cards = c[M+1:] + c[m:M+1] + c[:m]
logger.debug("TripleCut: {}".format(str(self)))
def keystream(self):
i, passed = 1, False
while True:
if not passed:
logger.debug("DRAW #{}".format(i))
passed = False
self._move_down()
self._triple_cut()
self._count_cut()
card = self._output_card()
if card not in [self.A + 1, self.B + 1]:
logger.debug("> Card: {}\n".format(card))
yield card
i += 1
else:
passed = True
@property
def encoded(self):
mapping = Deck.encoding_scheme(True)
return '\n'.join(map(lambda c: mapping[c], self.start))
@staticmethod
def encoding_scheme(encode=True):
bridge_suit = []
for c in ['c', 'd', 'h', 's']:
for f in ['a'] + list(map(str, range(2, 11))) + ['j', 'q', 'k']:
bridge_suit.append(c + f)
bridge_suit.extend(['jr', 'jb'])
return {i: k for i, k in enumerate(bridge_suit)} if encode else \
{k: i for i, k in enumerate(bridge_suit)}
class Solitaire(object):
chr2int = lambda s, c: uppercase.index(c) + 1
int2chr = lambda s, i: uppercase[i-1]
def __init__(self, deck, A=52, B=53):
if not isinstance(deck, Deck):
raise TypeError("Not a valid Deck")
self.deck = deck
def decrypt(self, ciphertext):
ciphertext = ciphertext.replace(' ', '').upper()
plaintext = ""
for i, k in enumerate(self.deck.keystream()):
if i >= len(ciphertext):
break
plaintext += self.int2chr((self.chr2int(ciphertext[i]) - k) % 26)
return plaintext
def encrypt(self, plaintext):
plaintext = plaintext.replace(' ', '').upper()
plaintext = ''.join(list(filter(lambda x: x.isalnum(), plaintext)))
ciphertext = ""
for i, k in enumerate(self.deck.keystream()):
if i >= len(plaintext):
break
ciphertext += self.int2chr((self.chr2int(plaintext[i]) + k) % 26)
return ' '.join(ciphertext[i:i+5] for i in range(0, len(ciphertext), 5))
if __name__ == '__main__':
subparsers = parser.add_subparsers(help="commands", dest="command")
decrypt = subparsers.add_parser('decrypt', help="decrypt message")
encrypt = subparsers.add_parser('encrypt', help="encrypt message")
for subparser in [decrypt, encrypt]:
subparser.add_argument("message", help="message to be handled")
subparser.add_argument("-a", type=int, default=53, help="joker A")
subparser.add_argument("-b", type=int, default=54, help="joker B")
subparser.add_argument("-d", default=','.join(map(str, range(1, 55))),
dest="deck", help="deck file or list of integers")
subparser.add_argument("-p", dest="passphrase", required=True,
help="passphrase")
encrypt.add_argument("-o", dest="output", default="deck.txt",
help="save the encoded deck to")
encrypt.add_argument("-s", dest="shuffle", action="store_true",
help="shuffle the deck")
initialize(noargs_action="config", add_config=True)
validate(
('a', "not 0 < ? <= 54", "A must be an integer from 1 to 54"),
('b', "not 0 < ? <= 54", "B must be an integer from 1 to 54"),
('deck', "not Deck( ? )"),
)
output = getattr(args, "output", None)
shuffle = getattr(args, "shuffle", False)
solitaire = Solitaire(Deck(args.deck, args.a - 1, args.b - 1, shuffle))
logger.info(getattr(solitaire, args.command)(args.message))
logger.info(repr(solitaire.deck))
if output is not None:
with open(output, 'w') as f:
f.write(solitaire.deck.encoded)
logger.info("Saved the encoded deck to '{}'".format(output))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment