Merge pull request 'Add documentation and LC4 implementation for playing-card-based tables' (#3) from tkterris/ls47:solitaire into master

Reviewed-on: https://gitea.blesmrt.net/exa/ls47/pulls/3
This commit is contained in:
Mirek Kratochvil 2021-04-23 17:10:04 +02:00
commit 6fb1241799
3 changed files with 100 additions and 8 deletions

View file

@ -142,6 +142,8 @@ stays the same.
Grab a bag full of tiles and randomly draw them one by one. Key is the 49-item permutation of them.
## Modifications
### Key expansion from a password
Remembering 49-position random permutation that includes weird characters is
@ -196,6 +198,84 @@ This works because the cipher output is message-dependent: Having a wrong bit
somewhere in the middle causes avalanche effect and erases any meaning from the
text after several characters.
### Alternative playing-card-compatible board
The following board can be used so that characters and offsets can be easily
mapped to playing cards, similar to the Solitaire cipher [2]. Using playing
cards could be more innocuous and easily explainable to the secret police than
a set of peculiar numbered tiles.
```
a b c d e f g
h i j k l m n
o p q r s t u
v w x y z _ .
, - + * / : ?
! ' ( ) 1 2 3
4 5 6 7 8 9 0
```
**This board uses a 1-based index**, so `a=1`, `b=2`, and so on. With this layout,
the following mapping to playing cards is used:
| **Character** | Card | Index | **Character** | Card | Index | **Character** | Card | Index | **Character** | Card | Index |
|-----------|------|-------|-----------|------|-------|-----------|------|-------|-----------|------|--------|
| a | A♦ | 1 | n | A♣ | 14 | _ | A♥ | 27 | 1 | A♠ | 40 |
| b | 2♦ | 2 | o | 2♣ | 15 | . | 2♥ | 28 | 2 | 2♠ | 41 |
| c | 3♦ | 3 | p | 3♣ | 16 | , | 3♥ | 29 | 3 | 3♠ | 42 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| j | 10♦ | 10 | w | 10♣ | 23 | ! | 10♥ | 39 | 7 | 7♠ | 46 |
| k | J♦ | 11 | x | J♣ | 24 | ' | J♥ | 39 | 8 | 8♠ | 47 |
| l | Q♦ | 12 | y | Q♣ | 25 | ( | Q♥ | 39 | 9 | 9♠ | 48 |
| m | K♦ | 13 | z | K♣ | 26 | ) | K♥ | 39 | 0 | 10♠ | 49 (0) |
The (x, y) offset can be calculated by first determining the index: take the
number of the suit (0-3), multiply by 13, and add the card number (using 1 for
the Ace, 11-13 for face cards). Then, determine the offset from the index
using the usual modular arithmetic: `x = index % 7` and `y = index / 7`.
Alphanumeric characters can be easily mapped to cards, with letters mapping to
Diamonds or Clubs and numbers mapping to Spades (note that Spades face cards
are ommitted). All of the special characters are mapped to the Hearts, though
coming up with a mnemonic for the mapping of each special character to each
Heart is left as an exercise for the reader.
For an example, view [this image](card-tiles.jpg). In this example, the marker
is on J♣ (at the top left), and we want to encrypt the plaintext character `e`.
The image includes the character, index, and offsets for the relevant cards.
1. From the card mapping, `e` maps to 5♦ (letters `a` through `m` map to Diamonds), which is on the second row.
1. The marker card, J♣, has:
- an index of 24 (Clubs are suit 1, and the Jack is the 11th card in the suit): `i = 1 * 13 + 11 = 24`
- an x-offset of 3: `x = i % 7 = 24 % 7 = 3`
- a y-offset of 3: `y = i / 7 = 24 / 7 = 3`
1. Using the marker offsets, the ciphertext card is three rows down and three columns to the right of 5♦, which is 6♠.
1. From the card mapping, 6♠ maps to `6` (Spades map directly to digits), and has:
- an index of 45 (Spades are suit 3, and 6 is the 6th card in the suit): `i = 3 * 13 + 6 = 45`
- an x-offset of 3: `x = i % 7 = 45 % 7 = 3`
- a y-offset of 6: `y = i / 7 = 45 / 7 = 6`
1. We then complete the cipher as normal:
- Output `6` as the ciphertext
- Rotate the row containing the plaintext card, 5♦
- Rotate the column containing the ciphertext card, 6♠
- Move the marker according to the offsets of the ciphertext card, to the right 3 and down 6
For LC4, the following board could be used:
```
a b c d e f
g h i j k l
m n o p q r
s t u v w x
y z _ 2 3 4
5 6 7 8 9 #
```
The "Hearts" suit would be ommitted from the card mapping table, and the index
of each of the Spades would be decreased by 13 to compensate. `_` and `#`
would map to A♠ and 10♠, respectively.
## References
[1] *Kaminsky, Alan. "ElsieFour: A Low-Tech Authenticated Encryption Algorithm For Human-to-Human Communication." IACR Cryptology ePrint Archive 2017 (2017): 339.*
[1] *Kaminsky, Alan. "ElsieFour: A Low-Tech Authenticated Encryption Algorithm For Human-to-Human Communication." IACR Cryptology ePrint Archive 2017 (2017): 339.*
[2] *Schneier, Bruce. ["The Solitaire Encryption Algorithm"](https://www.schneier.com/academic/solitaire/).*

BIN
card-tiles.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

26
lc4.py
View file

@ -54,8 +54,10 @@ import argparse
version = "v2.8.1 (2018-07-24)"
# define alphabet
letters6 = "#_23456789abcdefghijklmnopqrstuvwxyz"
letters7 = "_abcdefghijklmnopqrstuvwxyz.0123456789,-+*/:?!'()"
letters6 = "#_23456789abcdefghijklmnopqrstuvwxyz"
letters6card = "#abcdefghijklmnopqrstuvwxyz_23456789"
letters7 = "_abcdefghijklmnopqrstuvwxyz.0123456789,-+*/:?!'()"
letters7card = "0abcdefghijklmnopqrstuvwxyz_.,-+*/:?!'()123456789"
def missing_letters(s,t):
@ -149,9 +151,11 @@ def rotate_marker_down(m, col, n):
return ((m[0] + n) % size, m[1])
def derive_key(password):
def derive_key(password, one_indexed):
i = 0
k = letters
# if using one-indexed arrays, moves the zero element to the end
if one_indexed: k = k[1:]+k[0]
for c in password:
(row, col) = find_ix(c)
k = rotate_down(rotate_right(k, i, col), i, row)
@ -293,7 +297,7 @@ def test1(size, fixednonce):
else:
keyword = 's3cret_p4ssw0rd/31337'
szkeyword = keyword # This statement needed to show keyword in printinfo() [don't change args.keywordstring within test1()!]
key = derive_key(keyword)
key = derive_key(keyword, false)
else:
key = letters
initialkey = key
@ -333,6 +337,8 @@ if __name__ == '__main__':
mgroup1.add_argument("-6", "--lc4", help="use ElsieFour cipher (6x6 table) (default)", action="store_true")
mgroup1.add_argument("-7", "--ls47", help="use LS47 cipher (7x7 table)", action="store_true")
parser.add_argument("-pc", "--playingcard", help="Use the \"playing card\" character tables (default: standard tables)", action="store_true")
mgroup2 = parser.add_mutually_exclusive_group()
mgroup2.add_argument("-ks", "--keystring", metavar="STRING", help="use STRING as key")
mgroup2.add_argument("-kf", "--keyfile", metavar="FILE", help="read key from FILE")
@ -375,10 +381,16 @@ if __name__ == '__main__':
if args.ls47:
size = 7
letters = letters7
if args.playingcard:
letters = letters7card
else:
letters = letters7
else:
size = 6
letters = letters6
if args.playingcard:
letters = letters6card
else:
letters = letters6
tiles = list(zip(letters, [(x // size, x % size) for x in range(size * size)]))
@ -423,7 +435,7 @@ if __name__ == '__main__':
if args.keywordfile: args.keywordstring = open(args.keywordfile, 'r').read().rstrip('\r\n')
if args.keywordstring:
szkeyword = args.keywordstring
key = derive_key(args.keywordstring)
key = derive_key(args.keywordstring, args.playingcard)
if args.keyfile: args.keystring = open(args.keyfile, 'r').read().rstrip('\r\n')
if args.keystring: key = args.keystring;