From a57b23e308bb5dd0846ad977dbf931aeef8ed42f Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 29 Oct 2026 21:59:11 +0100 Subject: a bit of a clean-up --- LS47.scad | 12 +- LS47_en.stl | Bin 592084 -> 0 bytes LS47_ru.stl | Bin 624284 -> 0 bytes README.md | 398 ++++++++++++++++++++++---------------------- card-tiles.jpg | Bin 172391 -> 0 bytes lc4.py | 493 ------------------------------------------------------- ls47-arrows.scad | 91 ---------- ls47.py | 179 +++++++++++++------- ls47.scad | 91 ++++++++++ 9 files changed, 415 insertions(+), 849 deletions(-) delete mode 100644 LS47_en.stl delete mode 100644 LS47_ru.stl delete mode 100644 card-tiles.jpg delete mode 100644 lc4.py delete mode 100644 ls47-arrows.scad create mode 100644 ls47.scad diff --git a/LS47.scad b/LS47.scad index b94ee87..904193c 100644 --- a/LS47.scad +++ b/LS47.scad @@ -2,7 +2,7 @@ // No license, but send me a hello if you use this for some good purpose. // -- Mirek Kratochvil -font = "FiraMono"; +font = "FiraMono:style=Bold"; alphabet = "en"; @@ -45,19 +45,19 @@ module tilec(letter, x, y, c) { cylinder(tileheight, corner, corner); }; color("white") { - translate([tilesize*(0.5-dx*0.13),tilesize*(0.33-dy*0.13),tileheight*.51]) - linear_extrude (height=tileheight/2) + translate([tilesize*(0.5-dx*0.1),tilesize*(0.3-dy*0.1),tileheight*.5]) + linear_extrude (height=tileheight) text(letter, font=font, size=tilesize*.48, halign="center", valign="baseline"); } color("white") - translate([tilesize*(0.5+dx*0.3), tilesize*0.5, tileheight*.76]) + translate([tilesize*(0.5+dx*0.3), tilesize*0.5, tileheight*.5]) scale(4*[dx,1,1]) rotate([0,0,-90]) linear_extrude(height=tileheight/4) arrow(abs(xx)); color("white") - translate([tilesize*0.5, tilesize*(0.5+dy*0.3), tileheight*.76]) + translate([tilesize*0.5, tilesize*(0.5+dy*0.3), tileheight*.5]) scale(4*[1,dy,1]) linear_extrude(height=tileheight/4) arrow(abs(yy)); @@ -79,7 +79,7 @@ module tiles(letters) { if (alphabet=="en") { // latin variant of tiles tiles( - ["...", "A", "B", "C", "D", "E", "F", + ["_", "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", ".", diff --git a/LS47_en.stl b/LS47_en.stl deleted file mode 100644 index e5496d3..0000000 Binary files a/LS47_en.stl and /dev/null differ diff --git a/LS47_ru.stl b/LS47_ru.stl deleted file mode 100644 index f2206aa..0000000 Binary files a/LS47_ru.stl and /dev/null differ diff --git a/README.md b/README.md index f403134..29d6b3c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # LS47 hand-computable cipher -**This repository is a mirror, please point all activity/issues to https://git.e-x-a.org/exa/ls47** - This is a slight improvement of the ElsieFour cipher as described by Alan Kaminsky [1]. We use 7x7 characters instead of original (barely fitting) 6x6, to be able to encrypt some structured information. We also describe a simple @@ -17,19 +15,29 @@ We suggest printing the model using more than one filament color to make the letters easily recognizable. Thanks go to Martin Ptasek for providing the picture. -If you trust your computer, there are several LS47 implementations around: +If you trust your computer, there's an implementation in `ls47.py`. + +### Version history + +Historically, there were several other versions, e.g.: -- A very simple python implementation in available here in `ls47.py`. -- A much better version usuable as an actual binary (also supporting several +- A nice Python version usuable as an actual binary (also supporting MANY versions of padding and the original ElsieFour cipher) was supplied by - Bernhard Esslinger from the CrypTool project, available in `lc4.py`. + Bernhard Esslinger from the CrypTool project. - Javascript version (npm-compatible) of the cipher implementations was created - by Ulysse McConnell, available at https://github.com/umcconnell/lc4 + by Ulysse McConnell, now available at https://github.com/umcconnell/lc4 +- Trevor Terris supplied a nicely arranged playing-cards version together with + useful hints on how to encrypt with these. -### Character board +These versions can be found in git history of the repository. -We have added some real punctuation, basic stuff for writing expressions, -punctuation and quotes. The letters of the board now look like this: +In 2026, the board was slightly reorganized and we started using the version +with arrows instead of the numbers. + +## Character board + +Compared to LC4, we have added some real punctuation, basic stuff for writing +expressions, and some quotes. The letters of the board now look like this: ``` _ A B C D E F @@ -41,20 +49,21 @@ U V W X Y Z . 7 8 9 + - * / ``` -The cipher works with position changes. Zoomed in; it's very practical to also -have the shift information written on the tiles as arrows: +The cipher works by changing positions of a marker over a set of tiles (with +arrows) and permuting the tiles. For the position changes, the tiles have +arrows on them (0-3 horizontally and vertically, in either direction): ``` .-----. .-----. .-----. .-----. .-----. -| | | >| | >| | >| |< | -| _ | | A | | B >| | C >| |< D | ... -| | | | | | | >| |< | +| | | | |< | |< | | >| +| _ | |< A | | B | |< C | | D >| ... +| | | | |< | |< | | >| '-----' '-----' '-----' '-----' '-----' .-----. .-----. -| | | >| -| G | | H >| ... -|v | |v | +| ^ | | ^ | +| G | |< H | ... +| | | | '-----' '-----' . . . . @@ -72,229 +81,212 @@ more concentrated: ### Encryption -1. The symmetric key is the initial state of the board. Arrange your tiles to 7x7 square according to the key. +Initialization: + +1. The symmetric key is the initial state of the board. Arrange your tiles to + 7x7 square, according to the key. 2. Put the marker on the tile in the upper left corner. -3. Find the next letter you want to encrypt on the board (call it `P`). -4. Look at the marker; let's call the tile under the marker `M`. -5. The marked tile `M` has arrows drawn on it. Put a finger on tile `P` and move it to next tiles according to arrows on `M` -- e.g., if it has 2 arrows to the right and one to up, you jump 2 tiles to the right and one up. In case you hit the corner of the board, do a "wraparound" and continue on the other side. After the jumps, your finger rests on the ciphertext tile; mark it `C`. This letter is now encrypted and you can "transmit" it. -6. Shift the row that contains the plaintext letter one position to the right. The tile on the right will get out of the board; so do a "wraparound" again and put it to the empty spot on the left. (Let's call this "rotation"). If the marker is present on any of the affected tiles, DO NOT CARRY IT with the tiles (it should stay on the same coordinates). -7. In a similar fashion, rotate the column that now (after the first rotation) contains the ciphertext tile `C` one position down, again doing the wraparound and preserving the marker position. -8. Update the position of the marker: Look at the arrow directions on the ciphertext tile `C` and move the marker according to the directions (just like previously with finding the ciphertext). -9. Repeat from 3 as many times as needed to encrypt the whole plaintext. -#### Encryption example with ascii images! +Encryption loop (repeat for all letters of the plaintext): + +1. You need to encrypt one letter of the plaintext. First, find the position of + the tile with that letter on the board. This is the *plaintext tile*. +2. Second, look at the tile under the marker (*marker tile*); we are interested + in the arrows that are on the marker tile. Start at the *plaintext tile*, + and make steps from it in the exact direction and count as there are arrows + on the *marker tile*. In case you run out of board in any direction, wrap + around to the other side of the board. The steps will end on a *ciphertext + tile*. +3. Look at the letter on the *ciphertext tile*. This is the next letter of the + ciphertext; write it down (or send it via telegraph, or such). +4. Do the rotations: + - First, shift the row where the *plaintext tile* lies by one position to the + right. The tile on the right will get shifted out out of the board, so wrap + it around again to the now-empty spot on the left of the board. (We can + call this *rotation*.). If the marker is sitting on any of the affected + tiles, DO NOT MOVE IT with the tiles (or move it back after you do the + rotation) -- it should stay on the exactly same coordinates. + - Second, similarly, rotate the *column* that now (after the first rotation) + contains the *ciphertext tile* one position down, again wrapping the last + tile, and keeping the marker position intact. +5. Look at the arrows on the *ciphertext tile* `C` and move the marker + according to the arrow directions (just like previously when you were + following the arrows from the plaintext to ciphertext). +6. Repeat from 3 as many times as needed to encrypt the whole plaintext. + +### Decryption + +Decryption procedure is basically the same, except that instead of knowing the +plaintext tile and finding the ciphertext tile, you you *know the ciphertext +tile and need to trace back to the plaintext tile*. + +This is done simply by reversing the step 2 of the encryption loop: Instead of +taking the arrows on the marker tile "forward" from plaintext to ciphertext, +you start at ciphertext, and *take the arrows "backwards"* to find the +plaintext. + +Note that the rotations and marker position change is still done "forwards", +just like in encryption. +### Encryption example! + +Let's use this as the starting key: +``` + E F _ A B C D + L M G H I J K + ( ) / : ? ! ' + S T N O P Q R + Z . U V W X Y + 5 6 0 1 2 3 4 + + * 7 8 9 , - ``` -1,2. Symmetric key with 3,4. We want to encrypt 'Y'. - marker put on 'E': Look at the marked tile first: - [E]F _ A B C D /-----\ - L M G H I J K |< | - ( ) / : ? ! ' |< E | - S T N O P Q R | | - Z . U V W X Y \-----/ - 5 6 0 1 2 3 4 - + * 7 8 9 , - +The marker starts in the upper right, thus sitting on `E`. We label the marker +with brackets, a marked tile is shown as `[E]`: -5. Ciphertext is 'W' 6. Rotate the plaintext 1 position - (='Y' moved to <<) right, keep marker coordinates. +``` +[E]F _ A B C D + L M G H I J K + ( ) / : ? ! ' + S T N O P Q R + Z . U V W X Y + 5 6 0 1 2 3 4 + + * 7 8 9 , - +``` - [E]F _ A B C D [E]F _ A B C D - Output 'U'! L M G H I J K L M G H I J K - ( ) / : ? ! ' ( ) / : ? ! ' - S T N O P Q R S T N O P Q R - Z . U V W X Y >> Y Z . U V W X - 5 6 0 1 2 3 4 5 6 0 1 2 3 4 - + * 7 8 9 , - + * 7 8 9 , - +Initialization is done, we start the encryption loop. +**We want to encrypt `Q`.** Look at the arrows at the marked tile: -7. Rotate the ciphertext 1 Now look at the ciphertext tile: - position down. +``` +.-----. +| >| +| E | +| >| +'-----' +``` - [E]F _ A B , D /-----\ - L M G H I C K | >| - ( ) / : ? J ' | W >| - S T N O P ! R |vvv | - Y Z . U V Q X \-----/ - 5 6 0 1 2 W 4 - + * 7 8 9 3 - +This means that the ciphertext tile is 2 positions to the right from the +plaintext tile. We find `Q` on the board, move 2 positions to the right +(wrapping around to the left side), and finish at `S`. **`S` is the next letter +of the ciphertext.** -8. Update the marker position 9. GOTO 3. - by ciphertext arrows (vvv>>) +Now, let's do the rotation: First we rotate plaintext tile to the right: +``` +[E]F _ A B C D + L M G H I J K + ( ) / : ? ! ' + R S T N O P Q <--- this row moved, and R wrapped around + Z . U V W X Y + 5 6 0 1 2 3 4 + + * 7 8 9 , - +``` - E F _ A B , D - L M G H I C K - ( ) / : ? J ' - S T[N]O P ! R - Y Z . U V Q X - 5 6 0 1 2 W 4 - + * 7 8 9 3 - +After that, we rotate the ciphertext tile downwards: +``` +[E]* _ A B C D + L F G H I J K + ( M / : ? ! ' + R ) T N O P Q + Z S U V W X Y + 5 . 0 1 2 3 4 + + 6 7 8 9 , - + + ^ + '------- this column moved, and the * got wrapped to the top +``` +Now we look at the ciphertext: +``` +.-----. +| ^ ^>| +| S | +| >| +'-----' ``` -### Decryption +It says that we move the marker 2 positions up and 2 to the right, so we end up +with a new marker position: +``` + E * _ A B C D + L F G H I J K + ( M / : ? ! ' + R ) T N O P Q + Z S U V W X Y + 5 .[0]1 2 3 4 + + 6 7 8 9 , - +``` -Decryption procedure is basically the same, except that in step 5 you know `C` -and `M`, and need to produce `P` by going back to it from `C`. Simply take the -arrows on the marked tile and use them in reverse direction starting from `C`. +That's it for encrypting the `Q`; now we can continue the encryption loop with +another letter. ### Key generation Grab a bag full of tiles and randomly draw them one by one. Key is the 49-item permutation of them. -## Modifications +## Extensions and modifications ### Key expansion from a password Remembering 49-position random permutation that includes weird characters is -not very handy. You can instead derive the keys from an arbitrary string of -sufficient length. - -"Sufficient" means "provides enough entropy". Full keys store around 208 bits -of entropy. To reach that, your password should have: - -- at least around 61 decimal digits if made only from random decimal digits -- at least around 44 letters if made only from completely random letters -- at least around 40 alphanumeric characters if made randomly only from them - -To have the "standard" 128 bits of entropy, the numbers reduce to roughly 39, -28 and 25, respectively. - -Note that you can save the expanded tile board for later if you don't want to -expand the passwords before each encryption/decryption. - -The actual expansion can be as simple as this: - -1. Put the tiles on the board in a fixed "initial" position, - e.g. as on the character board picture above, sorted by the arrow offsets. -2. Find the first letter of the password. Rotate it first horizontally, then - vertically, according to the number of arrows on the tile. -3. Find the tile below right (`v>` in arrows) of the new position of the - password letter (possibly wrapping around the board), and rotate it - horizontally and vertically according to what its arrows say -- just as with - the password letter before. -4. Repeat as needed from step 2 for the whole password. -5. Final tile positions are the expanded key. +not very handy. There are many methods to do key expansion, so we show a simple +(although not super secure) one here: + +1. Think a password as a string of letters from the board. +2. Start with a board in the "canonical order". +3. In series, take all letters of your password from the board and place them + to the new board, starting at bottom right, and contining by rows going to + the left, and wrapping upwards to the next line. If some letter is already + moved out of the board, ignore it. +4. If the password did not include the space tile (`_`), add it as if it was + the last letter of the password. +5. Finally, take the rest of the board in the order (line-by-line from the + bottom right) and continue adding it to the password lines (again adding + line-by-line, as with the password). This will squash any blanks left over + after removing the password tiles. + +The new board is the expanded key. + +For any notion of real security, it is quite adviseable to **not** rely on the +mixing provided by this procedure (it **is** insufficient). Instead, run a few +encryption rounds of random padding characters (i.e., a *nonce*) before +starting the encryption of the actual plaintext (see below). ### Undistinguishable ciphertexts To get a different ciphertext even if the same plaintext is encrypted -repeatedly, prepend it with a nonce. A nonce is a completely random sequence of -letters of a pre-negotiated length (e.g. N tiles drawn randomly from a bag, -adviseable value of N is at least 10). +repeatedly, prepend the plaintext with a *nonce*. A nonce is a completely +random sequence of letters, usually of pre-negotiated length or with an easily +distinguishable separator. + +You can e.g. agree to always draw exactly 10 random tiles from a bag as the +nonce, or to use nonces of random length while keeping the space tile as a +separator that is guaranteed to not occur in the nonce. -You may also want to add a random number of spaces to the end of the ciphertext --- it prevents the enemy from seeing the difference between ciphertexts of 'yes -please' and 'no', which would otherwise encrypt to gibberish that is easily -distinguishable by length, like `qwc3w_cs'(` and `+v`. +Randomization of ciphertext length is quite beneficial for short messages; even +if not using nonces, it is quite beneficial to add a random number of spaces to +the end of the ciphertext: It prevents the adversary from seeing the difference +between plaintexts such as 'yes please' and 'no', the ciphertexts of whese +would otherwise be easily distinguishable by length, like `qwc3w_cs'(` and +`+v`. ### Authenticated encryption Because ciphertext may be altered in the transfer or during the error-prone -human processing, it is advised to append a simple "signature" to the end of -the message; which may look as simple as `__YourHonorableNameHere`. If the -signature doesn't match expectations (which happens with overwhelming -probability if there was any error in the process), either try again to see if -you didn't make a mistake, or discard the message and ask the sender to -re-transmit. +human processing, it is advised to append a simple human-readable "signature" +to the end of the message. That may look as simple as `__YourNameHere`. In case +there's any error or manipulation in the ciphertext, the signature will quite +likely come out completely garbled with overwhelming probability, serving +essentially as an authenticated checksum. -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 board with playing cards ♥♦♠♣ - -If you find the above tiles complicated to obtain or create, you can very -easily use playing cards to do the same, similarly as with the [Solitaire -cipher by Schneier](https://www.schneier.com/academic/solitaire/). (Moreover, -use of playing cards could be more innocuous and easily explainable to a -suddenly appearing adversary. Playing cards is sure less suspicious than -playing some peculiarly numbered tiles! :] ) - -To simplify things a bit, we shall use the following layout: - -``` -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 maps nicely to the playing card suits: - -| Card value | ♦ | ♣ | ♥ | ♠ | -|------------|---|---|---|---| -| A | A | N | _ | 1 | -| 2 | B | O | . | 2 | -| 3 | C | P | , | 3 | -| 4 | D | Q | - | 4 | -| 5 | E | R | + | 5 | -| 6 | F | S | * | 6 | -| 7 | G | T | / | 7 | -| 8 | H | U | : | 8 | -| 9 | I | V | ? | 9 | -| 10 | J | W | ! | 0 | -| J | K | X | ' | | -| Q | L | Y | ( | | -| K | M | Z | ) | | - -The last 3 cards are not used. (Actually, you are free to use the suits in -whatever order you like and discard the last 3 of the suit you like the least.) - -Getting the X and Y offsets from the cards is a slightly more challenging than -with the tiles that have the offsets marked, but still doable. Calculate -`13*suit + card value - 1`, then compute the standard quotient and remainder -for division by 7. I settled with having the second card set laid out next to -the "active" board, serving as a reference. - -The result may look like this: - -![Using cards instead of tiles](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 -Tiles), which is on the second row. -2. 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` -3. Using the marker offsets, the ciphertext card is three rows down and three - columns to the right of 5♦, which is 6♠. -4. 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` -5. 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 the original LC4, you need just 3 suits, modifying the board to "align" to -the suits e.g. as follows: - -``` -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 # -``` +This works because the cipher output is message-dependent: Taking a different +input (or making a different step) anywhere in the middle of the encryption +causes an avalanche effect, and effectively erases any meaning from the text +after several iterations. ## 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.* diff --git a/card-tiles.jpg b/card-tiles.jpg deleted file mode 100644 index 920a2fc..0000000 Binary files a/card-tiles.jpg and /dev/null differ diff --git a/lc4.py b/lc4.py deleted file mode 100644 index df533e2..0000000 --- a/lc4.py +++ /dev/null @@ -1,493 +0,0 @@ -#!/usr/bin/python2 -# -*- coding: utf-8 -*- -# This software is hereby released into public domain. Use it wisely. -# -# ElsieFour (LC4) and its enhancement LS47 -# version 2.8.1 (2018-07-24) -# -# Original paper by Alan Kaminsky: https://eprint.iacr.org/2017/339.pdf -# Original LC4 Java source: https://www.cs.rit.edu/~ark/parallelcrypto/elsiefour/ -# -# - Python 2 script for LS47 originally written by Mirek Kratochvil (2017) -# See https://gitea.blesmrt.net/exa/ls47 -# - Python 3 port by Bernhard Esslinger / AK (Feb 2018) -# See www.mysterytwisterc3.org -# - New options by CrypTool project (www.cryptool.org) (Feb 2018) in order to -# support both ciphers LC4 and LS47 in one program, to support both ways to -# deal with nonces and keywords, to support reading from file and commandline, -# and to extend test outputs (v2.7). -# - v2.8 extended the testcase and added the fix of the fixpoint-ish problem for ls47 (option -m1): -# python lc4.py -6 -ws thisismysecretkey -es ############## -ns igxf5e -m1 -# igxf5egcyhoo#ny#o5i5 -# python lc4.py -6 -ws thisismysecretkey -es ############## -ns igxf5e -m0 -# igxf5e############## -# -# Sample calls: -# -# 1) Using cipher LC4, enforced with option -6: -# // encrypt (key, message). Following example returns tk5j23tq94_gw9c#lhzs -# python lc4.py -6 -ks s2ferw_nx346ty5odiupq#lmz8ajhgcvk79b -es aaaaaaaaaaaaaaaaaaaa -# // decrypt (key, message). Following example returns aaaaaaaaaaaaaaaaaaaa -# python lc4.py -6 -ks s2ferw_nx346ty5odiupq#lmz8ajhgcvk79b -ds tk5j23tq94_gw9c#lhzs -# python lc4.py -6 -ws thisismysecretkey -es its_my_fathers_son_but_not_my_brother -# python lc4.py -6 -ws thisismysecretkey -ds 6dudfy3u7omoxy4jbscgn37c2se_d8gx6ogk9 -# python lc4.py -6 -ws thisismysecretkey -es its_my_fathers_son_but_not_my_brother -v -nl 6 -# python lc4.py -6 -ws thisismysecretkey -ds q6xojffkncfyz#f5czs49#3mbsco#2iscvbnm#bymaf -v -nl 6 -# -# 2) Using cipher LS47, enforced with option -7: -# python lc4.py -7 -ws s3cret_p4ssw0rd/31337 -es conflagrate_the_rose_bush_at_six!---peace-vector-3 -v -ns 8y(l._4ct' -m0 -# python lc4.py -7 -ws s3cret_p4ssw0rd/31337 -ds y'zbvvs+d2,ky4sy?w(_wkz*7'90v:./s)kcz?mj+gyu8-'h(y,i+v,z+1ws -v -nl 10 -m0 -# python lc4.py -7 -ws s3cret_p4ssw0rd/31337 -ds y'zbvvs+d2,ky4sy?w(_wkz*7'90v:./s)kcz?mj+gyu8-'h(y,i+v,z+1ws -v -ns 8y(l._4ct' -m0 -# -# 3) Using the automatic test (option -t can be combined with options -s and -ws) -# python lc4.py -t -# python lc4.py -t -ws s3cret_p4sswxyz -s peacevector_34 - - -from __future__ import print_function -import sys -import os -import random -import argparse - - -version = "v2.8.1 (2018-07-24)" - -# define alphabet -letters6 = "#_23456789abcdefghijklmnopqrstuvwxyz" -letters6card = "abcdefghijklmnopqrstuvwxyz_23456789#" -letters7 = "_abcdefghijklmnopqrstuvwxyz.,:?!'()0123456789+-*/" -letters7card = "abcdefghijklmnopqrstuvwxyz_.,-+*/:?!'()1234567890" - - -def missing_letters(s,t): - return ''.join(sorted(set(c for c in s if c not in t))) - - -def check_key(key): - illegal = missing_letters(key,letters) - missing = missing_letters(letters,key) - duplicates = ''.join(sorted(c for c in letters if key.count(c)>1)) - - errors = [] - if illegal: errors.append( "Key contains illegal letters: '%s'" % illegal ) - if missing: errors.append( "Key misses some letters: '%s'" % missing ) - if duplicates: errors.append( "Key contains duplicate letters: '%s'" % duplicates ) - - if errors: - errors.append( "The key must be a permutation of the alphabet: %s" % letters ) - raise ValueError("\n".join(errors)) - - -def check_nonce(nonce): - illegal = missing_letters(nonce,letters) - if illegal: - raise ValueError("Nonce contains illegal letters: '%s'" % illegal) - - -def check_plaintext(s): - illegal = missing_letters(s,letters) - if illegal: - raise ValueError("Plaintext contains illegal letters: '%s'" % illegal) - - -def check_ciphertext(s): - illegal = missing_letters(s,letters) - if illegal: - raise ValueError("Ciphertext contains illegal letters: '%s'" % illegal) - - -def find_ix(letter): - m = [l[1] for l in tiles if l[0] == letter] - if len(m) != 1: - raise ValueError("Letter '%c' not in the alphabet!" % letter) - return m[0] - - -def find_pos(key, letter): - p = key.find(letter) - if not (0 <= p < size * size): - raise ValueError("Letter '%c' not in key?!" % letter) - return (p // size, p % size) - - -def add_pos(a, b): - return ((a[0] + b[0]) % size, (a[1] + b[1]) % size) - - -def sub_pos(a, b): - return ((a[0] - b[0]) % size, (a[1] - b[1]) % size) - - -def find_at_pos(key, coord): - return key[coord[1] + coord[0] * size] - - -def rotate_right(key, row, n): - mid = key[size * row:size * (row + 1)] - return key[:size * row] + mid[-n:] + mid[:-n] + key[size * (row + 1):] - - -def rotate_down(key, col, n): - lines = [key[i * size:(i + 1) * size] for i in range(size)] - lefts = [l[:col] for l in lines] - mids = [l[col] for l in lines] - rights = [l[col + 1:] for l in lines] - mids = mids[-n:] + mids[:-n] - return ''.join(lefts[i] + mids[i] + rights[i] for i in range(size)) - - -def rotate_marker_right(m, row, n): - if m[0] != row: - return (m[0], m[1]) - else: - return (m[0], (m[1] + n) % size) - - -def rotate_marker_down(m, col, n): - if m[1] != col: - return (m[0], m[1]) - else: - return ((m[0] + n) % size, m[1]) - - -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) - i = (i + 1) % size - return k - - -def encrypt(plaintext): - global key, mp - - ciphertext = '' - for p in plaintext: - pp = find_pos(key, p) - mix = find_ix(find_at_pos(key, mp)) - cp = add_pos(pp, mix) - c = find_at_pos(key, cp) - ciphertext += c - - key = rotate_right(key, pp[0], 1) - if marker_mode==1: mp = rotate_marker_right(mp, pp[0], 1) - cp = find_pos(key, c) - key = rotate_down(key, cp[1], 1) - if marker_mode==1: mp = rotate_marker_down(mp, cp[1], 1) - mp = add_pos(mp, find_ix(c)) - return ciphertext - - -def decrypt(ciphertext): - global key, mp - - plaintext = '' - for c in ciphertext: - cp = find_pos(key, c) - mix = find_ix(find_at_pos(key, mp)) - pp = sub_pos(cp, mix) - p = find_at_pos(key, pp) - plaintext += p - - key = rotate_right(key, pp[0], 1) - if marker_mode==1: mp = rotate_marker_right(mp, pp[0], 1) - cp = find_pos(key, c) - key = rotate_down(key, cp[1], 1) - if marker_mode==1: mp = rotate_marker_down(mp, cp[1], 1) - mp = add_pos(mp, find_ix(c)) - return plaintext - - -def create_random_nonce(size): - return ''.join(random.choice(letters) for i in range(size)) - - -def encrypt_with_nonce(plaintext): - global nonce, nonce_enc - - ciphertext = encrypt(nonce + plaintext) - nonce_enc = ciphertext[:nonce_size] - - if nonce_mode==1: - return nonce + ciphertext[nonce_size:] - else: - return ciphertext - - -def decrypt_with_nonce(ciphertext): - global nonce, nonce_enc - - if nonce_mode==1: - nonce = ciphertext[:nonce_size] - nonce_enc = encrypt(nonce) - return decrypt(ciphertext[nonce_size:]) - else: - plaintext = decrypt(ciphertext) - nonce = plaintext[:nonce_size] - nonce_enc = ciphertext[:nonce_size] - return plaintext[nonce_size:] - - -def eprint(*args, **kwargs): - print(*args, file=sys.stderr, **kwargs) - - -def printinfo(enc=False): # used by test1() and when option -v - eprint('CIPHER : ' + ("LC4" if size==6 else "LS47")) - eprint('ALPHABET : ' + letters) - if szkeyword: - eprint('KEYWORD : ' + szkeyword) - eprint('KEY : ' + initialkey) # key variable modified during process? - eprint('NONCE : ' + nonce) - eprint('NONCE ENC : ' + nonce_enc) - eprint('NONCEMODE : ' + ("Kaminsky" if nonce_mode==1 else "Kratochvil")) - eprint('MARKER : ' + ("Kaminsky" if marker_mode==1 else "Kratochvil")) - if enc: - # if args.signature: print ("args.SIGNA:", args.signature) - if szsignature: print ("SIGNATURE: ", szsignature) - eprint('PLAINTEXT : ' + plaintext) - eprint('CIPHERTEXT: ' + ciphertext) - else: - eprint('CIPHERTEXT: ' + ciphertext) - eprint('PLAINTEXT : ' + plaintext) - - -def test1(size, fixednonce): - # Declaration "global" only needed if you like to write to a global variable - # [here, this is necessary only if the global var. is used within a fct. called by test1()] - # As test1() is called several times don't change args components within this fct. - global letters, nonce_mode, marker_mode, tiles, noncelen, nonce, nonce_size, nonce_enc - global ciphertext, plaintext, key, initialkey, mp - global szkeyword, szsignature - - if size == 7: - CIPHERNAME = "LS47" - letters = letters7 - nonce_mode = 2 - marker_mode = 2 - if fixednonce == 0: - noncelen=10; nonce = create_random_nonce(noncelen) - else: - nonce = 'dr0+:_pij2' # just a sample for testing fixed nonce - else: - CIPHERNAME = "LC4" - letters = letters6 - nonce_mode = 1 - marker_mode = 1 - if fixednonce == 0: - noncelen=6; nonce = create_random_nonce(noncelen) - else: - nonce = 'pjpm5i' # just a sample for testing fixed nonce - - tiles = list(zip(letters, [(x // size, x % size) for x in range(size * size)])) - check_nonce(nonce) - nonce_size = len(nonce) - nonce_enc = "" - - print('\n' + CIPHERNAME) - - if size == 7: - if args.keywordstring: - keyword = args.keywordstring # Allows to use -ws besides -t - 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, false) - else: - key = letters - initialkey = key - check_key(key) - mp = (0, 0) - - if size == 7: - # signature = 'peace-vector-3' # just to name a meaningful value for size=7 - plaintext = 'conflagrate_the_rose_bush_at_six!' - if szsignature: plaintext += szsignature - else: - # signature = '#its_me' # Notice: with size=6 the chars "#", "0", "1", "-" aren't part of the alphabet - plaintext = 'its_my_fathers_son_but_not_my_brother' - if szsignature: plaintext += szsignature - - check_plaintext(plaintext) - ciphertext = encrypt_with_nonce(plaintext) - - key = initialkey; mp = (0, 0) # initialization again - check_ciphertext(ciphertext) - decryptedtext = decrypt_with_nonce(ciphertext) - print('decrypted text: ' + decryptedtext) - - printinfo(True) # Called from test1() with arg TRUE = assume to do decryption - - szkeyword='' - - - -if __name__ == '__main__': - - parser = argparse.ArgumentParser() # how to enforce, that (either no arg), (or only one arg -t, -v), (or give key and enc mode) - - parser.add_argument("-v", "--verbose", help="output additional information on stderr", action="count", default=0) - - mgroup1 = parser.add_mutually_exclusive_group() - 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") - mgroup2.add_argument("-ws", "--keywordstring", metavar="STRING", help="generate key from keyword STRING", default=None) - mgroup2.add_argument("-wf", "--keywordfile", metavar="FILE", help="read keyword from FILE to generate key", default=None) - - mgroup4 = parser.add_mutually_exclusive_group() - mgroup4.add_argument("-es", "--encryptstring", metavar="STRING", help="encrypt STRING") - mgroup4.add_argument("-ef", "--encryptfile", metavar="FILE", help="read plaintext from FILE and encrypt it") - mgroup4.add_argument("-ds", "--decryptstring", metavar="STRING", help="decrypt STRING") - mgroup4.add_argument("-df", "--decryptfile", metavar="FILE", help="read ciphertext from FILE and decrypt it") - mgroup4.add_argument("-t", "--test", help="encrypt and decrypt a string with a given key (with LC4, the given key equals the alphabet). Four cases tested: random/fixed nonce, each with LC4 and LS47", action="store_true") - - mgroup3 = parser.add_mutually_exclusive_group() - mgroup3.add_argument("-nl", "--noncelen", metavar="LENGTH", help="use random nonce of length LENGTH (default: no nonce)", type=int, default=0) - mgroup3.add_argument("-ns", "--noncestring", metavar="STRING", help="use STRING as nonce (default: no nonce)") - - mgroup5 = parser.add_mutually_exclusive_group() - mgroup5.add_argument("-n0", "--nKaminsky", help="use nonce in Kaminsky mode (default for LC4)", action="store_true") - mgroup5.add_argument("-n1", "--nKratochvil", help="use nonce in Kratochvil mode (default for LS47)", action="store_true") - - mgroup6 = parser.add_mutually_exclusive_group() - mgroup6.add_argument("-m0", "--mKaminsky", help="use marker in Kaminsky mode (default for LC4)", action="store_true") - mgroup6.add_argument("-m1", "--mKratochvil", help="use marker in Kratochvil mode (default for LS47)", action="store_true") - - - parser.add_argument("-s", "--signature", help="append SIGNATURE to plaintext when encrypting (default: no signature)") - - args = parser.parse_args() - - if len(sys.argv)==1: - parser.print_help() - sys.exit(1) - if args.verbose and len(sys.argv)==2: - print('Version: ' + version) - # parser.print_help() - sys.exit(1) - - # set cipher - - if args.ls47: - size = 7 - if args.playingcard: - letters = letters7card - else: - letters = letters7 - else: - size = 6 - if args.playingcard: - letters = letters6card - else: - letters = letters6 - - tiles = list(zip(letters, [(x // size, x % size) for x in range(size * size)])) - - # set nonce - - if args.noncestring: - nonce = args.noncestring - elif args.noncelen: - nonce = create_random_nonce(args.noncelen) - else: - nonce = "" - - check_nonce(nonce) - nonce_size = len(nonce) - nonce_enc = "" - - # set nonce mode - - nonce_mode = 1 if size==6 else 2 - if args.nKaminsky: nonce_mode = 1 - if args.nKratochvil: nonce_mode = 2 - - # set marker mode - - marker_mode = 1 if size==6 else 2 - if args.mKaminsky: marker_mode = 1 - if args.mKratochvil: marker_mode = 2 - - # set key - - key = letters - - # helper variables for verbose printing via printinfo() - - szkeyword = '' # global variable which can be changed in test1() [so it can be used as test condition in printinfo() instead of args.keywordstring and instead of test1() manipulating args.keywordstring] - szsignature = '' # global variable which can be changed in test1() [so it can be used as test condition in printinfo() instead of args.keywordstring and instead of test1() manipulating args.keywordstring]. # Allows to use -s besides -t - - if args.signature: # This case here is only necessary if -s should be used together with -t - szsignature = args.signature - - - if args.keywordfile: args.keywordstring = open(args.keywordfile, 'r').read().rstrip('\r\n') - if args.keywordstring: - szkeyword = 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; - - initialkey = key - check_key(key) - - mp = (0, 0) - - # encrypt / decrypt / test - - if args.encryptfile: - args.encryptstring = open(args.encryptfile, 'r').read().rstrip('\r\n') - - if args.decryptfile: - args.decryptstring = open(args.decryptfile, 'r').read().rstrip('\r\n') - - if args.encryptstring: - plaintext = args.encryptstring - if args.signature: - plaintext += args.signature - check_plaintext(plaintext) - ciphertext = encrypt_with_nonce(plaintext) - if args.verbose: - printinfo(True) - else: - print(ciphertext) - - elif args.decryptstring: - ciphertext = args.decryptstring - check_ciphertext(ciphertext) - plaintext = decrypt_with_nonce(ciphertext) - if args.signature and args.verbose: - eprint("--Warning--: the given signature is ignored during decryption") - # Maybe later return result of a check whether given signature equals the end of the decrypted string. - if args.verbose: - printinfo() - else: - print(plaintext) - - elif args.test: - print('\nVersion: ' + version) - print('\n---Test: No nonce provided (only its length), so each call will produce a different ciphertext.') - size = 7; test1(size, 0) - size = 6; test1(size, 0) - print('\n---Test: Nonce provided as string.') - size = 7; test1(size, 1) - size = 6; test1(size, 1) - sys.exit(1) - - else: # you'll never get here (handled via argparse) - # if args.verbose: print('Version : ' + version) - print('ALPHABET : ' + letters) - print('KEY : ' + initialkey) - print('NONCE : ' + nonce) diff --git a/ls47-arrows.scad b/ls47-arrows.scad deleted file mode 100644 index c77ad78..0000000 --- a/ls47-arrows.scad +++ /dev/null @@ -1,91 +0,0 @@ - -// No license, but send me a hello if you use this for some good purpose. -// -- Mirek Kratochvil - -font = "FiraMono:style=Bold"; - -//inch-sized tiles look imperial. -tilesize=25.4; -//you might like them a bit larger if you want to use this outdoors - -//distance between tiles -off=tilesize*1.1; -tileheight=tilesize/10; -fontheight=tileheight*0.5; -corner=tileheight*2; - -module arrow(n) { - if (n>=1) - for(i=[0:n-1]) translate([i-(n-1)/2,0,0]) - polygon([[0.45,0],[0,0.7],[-0.45,0]]); -} - -//this produces one tile -module tilec(letter, x, y, c) { - xx = ((10-x)%7)-3; - yy = ((y+3)%7)-3; - dx = sign(xx); - dy = sign(yy); - translate([x*off,(6-y)*off,0]) { - difference() { - color(c) union() { - cube([tilesize,tilesize-corner,tileheight]); - translate([corner, tilesize-corner,0]) - cube([tilesize-2*corner, corner, tileheight]); - translate([corner, tilesize-corner, 0]) - cylinder(tileheight, corner, corner); - translate([tilesize-corner, tilesize-corner, 0]) - cylinder(tileheight, corner, corner); - }; - color("white") { - translate([tilesize*(0.5-dx*0.1),tilesize*(0.3-dy*0.1),tileheight*.5]) - linear_extrude (height=tileheight) - text(letter, font=font, size=tilesize*.48, - halign="center", valign="baseline"); - } - color("white") - translate([tilesize*(0.5+dx*0.3), tilesize*0.5, tileheight*.5]) - scale(4*[dx,1,1]) - rotate([0,0,-90]) - linear_extrude(height=tileheight/4) - arrow(abs(xx)); - color("white") - translate([tilesize*0.5, tilesize*(0.5+dy*0.3), tileheight*.5]) - scale(4*[1,dy,1]) - linear_extrude(height=tileheight/4) - arrow(abs(yy)); - } - } -} - -//tiles -module tile(letter,x,y) { - tilec(letter,x,y,"darkgray"); -} - -module tiles(letters) { - for(i=[0,1,2,3,4,5,6]) for (j=[0,1,2,3,4,5,6]) { - tile(letters[i+7*j], i, j); - } -} - -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", ".", - ",", ":", "?", "!", "'", "(", ")", - "0", "1", "2", "3", "4", "5", "6", - "7", "8", "9", "+", "-", "*", "/"] -); - -//the token -translate([-off/2,off/2,0]) { - color("red") difference() { - cylinder(h=tileheight, d=tilesize); - translate([0,0,-.1]) - cylinder(h=tileheight*1.2, d=tilesize-2*tileheight); - } -} - - diff --git a/ls47.py b/ls47.py index 8736230..ae8b201 100755 --- a/ls47.py +++ b/ls47.py @@ -6,32 +6,35 @@ # Originally written by Mirek Kratochvil (2017) # Python3 port by Bernhard Esslinger (Feb 2018) +import argparse import random +import sys -letters = "_abcdefghijklmnopqrstuvwxyz.,:?!'()0123456789+-*/" +letters = "_ABCDEFGHIJKLMNOPQRSTUVWXYZ.,:?!'()0123456789+-*/" tiles = list(zip(letters, map(lambda x: (x // 7, x % 7), range(7 * 7)))) -padding_size = 10 +padding_size_min = 5 +padding_size_max = 15 def check_key(key): if len(key) != len(letters): - raise ValueError('Wrong key size') + raise ValueError("Wrong key size") cnts = {} for c in letters: cnts[c] = 0 for c in key: if not c in cnts: - raise ValueError('Letter ' + c + ' not in LS47!') + raise ValueError(f"Letter {c} not in LS47!") cnts[c] += 1 if cnts[c] > 1: - raise ValueError('Letter ' + c + ' duplicated in key!') + raise ValueError(f"Letter {c} duplicated in key!") def find_ix(letter): m = [l for l in tiles if l[0] == letter] if len(m) != 1: - raise ValueError('Letter ' + letter + ' not in LS47!') - for (l, pos) in m: + raise ValueError(f"Letter {letter} not in LS47!") + for l, pos in m: return pos @@ -39,7 +42,7 @@ def find_pos(key, letter): p = key.find(letter) if p >= 0 and p < 7 * 7: return (p // 7, p % 7) - raise ValueError('Letter ' + letter + ' not in key?!') + raise ValueError(f"Letter {letter} not in key!") def add_pos(a, b): @@ -55,19 +58,19 @@ def find_at_pos(key, coord): def rotate_right(key, row, n): - mid = key[7 * row:7 * (row + 1)] + mid = key[7 * row : 7 * (row + 1)] n = (7 - n % 7) % 7 - return key[:7 * row] + mid[n:] + mid[:n] + key[7 * (row + 1):] + return key[: 7 * row] + mid[n:] + mid[:n] + key[7 * (row + 1) :] def rotate_down(key, col, n): - lines = [key[i * 7:(i + 1) * 7] for i in range(7)] - lefts = [l[:col] for l in lines] - mids = [l[col] for l in lines] - rights = [l[col + 1:] for l in lines] + lines = [key[i * 7 : (i + 1) * 7] for i in range(7)] + lefts = [l[:col] for l in lines] + mids = [l[col] for l in lines] + rights = [l[col + 1 :] for l in lines] n = (7 - n % 7) % 7 mids = mids[n:] + mids[:n] - return ''.join(lefts[i] + mids[i] + rights[i] for i in range(7)) + return "".join(lefts[i] + mids[i] + rights[i] for i in range(7)) def rotate_marker_right(m, row, n): @@ -85,25 +88,21 @@ def rotate_marker_down(m, col, n): def derive_key(password): - k = letters - for c in password: - (crow, ccol) = find_pos(k, c) - (rows1, cols1) = find_ix(c) - k = rotate_right(k, crow, cols1) - (crow, ccol) = add_pos((crow, ccol), (0, cols1)) - k = rotate_down(k, ccol, rows1) - (crow, ccol) = add_pos((crow, ccol), (rows1+1, 1)) - (rows2, cols2) = find_ix(find_at_pos(k, (crow, ccol))) - k = rotate_right(k, crow, cols2) - (crow, ccol) = add_pos((crow,ccol), (0, cols2)) - k = rotate_down(k, ccol, rows2) - return k + k = ''.join(reversed(letters)) + i = 0 + for c in password + ' ': + ix = k.find(c,i) + if ix<0: + continue + k = k[:i] + k[ix] + k[i:ix] + k[ix+1:] + i += 1 + return ''.join(reversed(k)) def encrypt(key, plaintext): check_key(key) mp = (0, 0) - ciphertext = '' + ciphertext = "" for p in plaintext: pp = find_pos(key, p) mix = find_ix(find_at_pos(key, mp)) @@ -121,7 +120,7 @@ def encrypt(key, plaintext): def decrypt(key, ciphertext): check_key(key) mp = (0, 0) - plaintext = '' + plaintext = "" for c in ciphertext: cp = find_pos(key, c) mix = find_ix(find_at_pos(key, mp)) @@ -137,34 +136,102 @@ def decrypt(key, ciphertext): def encrypt_pad(key, plaintext, signature): - - # TODO it would also be great to randomize the message length. - check_key(key) - padding = ''.join(map(lambda x: letters[random.randint(0, - len(letters) - 1)], range(padding_size))) + padding = "".join( + map( + lambda x: letters[random.randint(1, len(letters) - 1)], + range(random.randint(padding_size_min, padding_size_max)), + ) + ) - return encrypt(key, padding + plaintext + '---' + signature) + return encrypt(key, padding + "_" + plaintext + "_" + signature) -def decrypt_pad(key, ciphertext): +def decrypt_pad(key, ciphertext, signature): check_key(key) - return decrypt(key, ciphertext)[padding_size:] - - -if __name__ == '__main__': - - # a bit of test! - - print('letters in this implementation (line by line):') - print(letters) - # print('tiles positions: ' + str(tiles)) - - key = derive_key('a') - print('test key: ' + key) - enc = encrypt_pad(key, 'conflagrate_the_rose_bush_at_six!', - 'peace-vector-3') - - print('encrypted test: ' + enc) - dec = decrypt_pad(key, enc) - print('decrypted test: ' + dec) + padded = decrypt(key, ciphertext) + pad_end = padded.find("_") + if pad_end < padding_size_min or pad_end > padding_size_max: + raise ValueError("mangled padding") + padded = padded[pad_end + 1 :] + if len(signature) == 0: + return padded + if padded[-len(signature) - 1 :] != "_" + signature: + raise ValueError("bad signature") + return padded[: -len(signature) - 1] + + +if __name__ == "__main__": + + ap = argparse.ArgumentParser( + prog=sys.argv[0], + description="Computer implementation of LS47.", + epilog="This is flimsy! Encrypt physically!", + ) + + ap.add_argument( + "-d", "--decrypt", dest="decrypt", action="store_true", help="run decryption" + ) + ap.add_argument( + "-p", + "--pad-and-sign", + dest="sign", + help="On encryption, add a random padding and a signature at the end. On decryption, expect a random padding and the exact signature. Specify empty string to attach no (or accept any) signature.", + ) + ap.add_argument( + "-s", + "--password", + dest="password", + help="Password string (to be expanded by the simple swapping algorithm)", + ) + ap.add_argument( + "-K", + "--full-key", + dest="fullkey", + help="Shared key (the initial state of the board, line by line, always with `_' instead of space; used only if password is not specified)", + ) + ap.add_argument( + "-I", + "--exact-input", + dest="inx", + help="Do not convert input spaces to `_', ignore them instead.", + action="store_true" + ) + ap.add_argument( + "-O", + "--exact-output", + dest="outx", + help="Do not convert output `_'s to spaces.", + action="store_true" + ) + + args = ap.parse_args() + + if args.password: + key = derive_key(args.password if args.inx else args.password.replace(' ', '_')) + elif args.fullkey: + key = args.fullkey + else: + raise ValueError("Specify at least one of password and full key (options -s and -K)") + + input = sys.stdin.read() + if not args.inx: + input = input.replace(' ', '_') + letter_set = set(letters) + input = ''.join(filter(lambda x:x in letter_set, input.upper())) + + if args.decrypt: + if args.sign is None: + output = decrypt(key, input) + else: + output = decrypt_pad(key, input, args.sign) + else: # encrypt + if args.sign is None: + output = encrypt(key, input) + else: + output = encrypt_pad(key, input, args.sign) + + if not args.outx: + output = output.replace('_', ' ') + + print(output) diff --git a/ls47.scad b/ls47.scad new file mode 100644 index 0000000..c77ad78 --- /dev/null +++ b/ls47.scad @@ -0,0 +1,91 @@ + +// No license, but send me a hello if you use this for some good purpose. +// -- Mirek Kratochvil + +font = "FiraMono:style=Bold"; + +//inch-sized tiles look imperial. +tilesize=25.4; +//you might like them a bit larger if you want to use this outdoors + +//distance between tiles +off=tilesize*1.1; +tileheight=tilesize/10; +fontheight=tileheight*0.5; +corner=tileheight*2; + +module arrow(n) { + if (n>=1) + for(i=[0:n-1]) translate([i-(n-1)/2,0,0]) + polygon([[0.45,0],[0,0.7],[-0.45,0]]); +} + +//this produces one tile +module tilec(letter, x, y, c) { + xx = ((10-x)%7)-3; + yy = ((y+3)%7)-3; + dx = sign(xx); + dy = sign(yy); + translate([x*off,(6-y)*off,0]) { + difference() { + color(c) union() { + cube([tilesize,tilesize-corner,tileheight]); + translate([corner, tilesize-corner,0]) + cube([tilesize-2*corner, corner, tileheight]); + translate([corner, tilesize-corner, 0]) + cylinder(tileheight, corner, corner); + translate([tilesize-corner, tilesize-corner, 0]) + cylinder(tileheight, corner, corner); + }; + color("white") { + translate([tilesize*(0.5-dx*0.1),tilesize*(0.3-dy*0.1),tileheight*.5]) + linear_extrude (height=tileheight) + text(letter, font=font, size=tilesize*.48, + halign="center", valign="baseline"); + } + color("white") + translate([tilesize*(0.5+dx*0.3), tilesize*0.5, tileheight*.5]) + scale(4*[dx,1,1]) + rotate([0,0,-90]) + linear_extrude(height=tileheight/4) + arrow(abs(xx)); + color("white") + translate([tilesize*0.5, tilesize*(0.5+dy*0.3), tileheight*.5]) + scale(4*[1,dy,1]) + linear_extrude(height=tileheight/4) + arrow(abs(yy)); + } + } +} + +//tiles +module tile(letter,x,y) { + tilec(letter,x,y,"darkgray"); +} + +module tiles(letters) { + for(i=[0,1,2,3,4,5,6]) for (j=[0,1,2,3,4,5,6]) { + tile(letters[i+7*j], i, j); + } +} + +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", ".", + ",", ":", "?", "!", "'", "(", ")", + "0", "1", "2", "3", "4", "5", "6", + "7", "8", "9", "+", "-", "*", "/"] +); + +//the token +translate([-off/2,off/2,0]) { + color("red") difference() { + cylinder(h=tileheight, d=tilesize); + translate([0,0,-.1]) + cylinder(h=tileheight*1.2, d=tilesize-2*tileheight); + } +} + + -- cgit v1.2.3