DIY Cryptography with Raku

Part 4: Postscript

by Arne Sommer

DIY Cryptography with Raku

Part 4: Postscript

[8.4] Published 21. April 2019

Perl 6 → Raku

This article has been moved from «perl6.eu» and updated to reflect the language rename in 2019.

See also: The Introduction | Part 1: Base 36 | Part 2: Base 400 | Part 3: Changing Keys.

Testing

Before sending an encrypted text I would have checked that it actually roundtrips. It is tedious to do that manually, so I wrote a version of «multiencrypt» that does that testing, free or charge:

File: multiencrypt-check
use lib "lib";

use Cryptoish;

sub MAIN (Cryptoish::Alphabet $key, Cryptoish::Alphabet $text, :$secret,
          Cryptoish::Modulo :$modulo = 40, Bool :$flip = False)
{
  my $value = Cryptoish::multiencrypt($key, $text, :$secret, :$modulo, :$flip);

  my $roundtrip
    = Cryptoish::multidecrypt($key, $value, :$secret, :$modulo, :$flip);

  if $text eq $roundtrip
  {
    say $value;
  }
  else
  {
    say "** The encrypted text (2. line) doesn't roundtrip when decrypted",
        " (3. line) **";
    say $value;
    say $roundtrip;
  }
}

It should be self-explanatory. Use this program instead of «multiencrypt» to encrypt messages. If the roundtrip, heaven forbid, should fail, please notify me.

Verbose

I have added a «verbose» mode, so that we can see what is going on, every step on the way with «multiencrypt» and «multidecrypt».

Enable verbose mode with the «--verbose» command line option (and note the newlines I had to add in the code blocks below, marked with «\», to make the lines fit the web page):

$ raku multiencrypt --verbose AA "ARNE SOMMER WRITES ABOUT PERL6."
Count: 16 ('G' -> 28)
Partial: 'ARNE SOMMER WRIT' -> (307 235 281 261 160 169 373 369 353 9 54 \
  153 283 389 11 303)
Key: U1 -> (391 305)
Count: 29 ('T' -> 273)
Partial: 'ES ABOUT PERL6.' -> (266 23 248 235 144 396 18 357 344 249 320 \
  150 201 131 332)
All: (28 307 235 281 261 160 169 373 369 353 9 54 153 283 389 11 303 391 \
  305 273 266 23 248 235 144 396 18 357 344 249 320 150 201 131 332)
Int: 98086136933496711163995899614360639306435717956795745557845378712086\
  57671657069099621722828
Str: 5BL47IGLOQG44PUZ6HANWN87HLVF1HK5DO10OS0ZUS0TB9C0PHOJLYI6BPO
5BL47IGLOQG44PUZ6HANWN87HLVF1HK5DO10OS0ZUS0TB9C0PHOJLYI6BPO
$ raku multidecrypt --verbose AA 5BL47IGLOQG44PUZ6HANWN87HLVF1HK5DO10OS0\
  ZUS0TB9C0PHOJLYI6BPO
Key: AA
Str: 5BL47IGLOQG44PUZ6HANWN87HLVF1HK5DO10OS0ZUS0TB9C0PHOJLYI6BPO
Int: 98086136933496711163995899614360639306435717956795745557845378712086\
  57671657069099621722828
All: 28 307 235 281 261 160 169 373 369 353 9 54 153 283 389 11 303 391 \
  305 273 266 23 248 235 144 396 18 357 344 249 320 150 201 131 332
Count: 28 -> G -> 16
Partial: (307 235 281 261 160 169 373 369 353 9 54 153 283 389 11 303) \
  -> 'ARNE SOMMER WRIT'
Key: (391 305) -> U1
Count: 273 -> T -> 29
Partial: (266 23 248 235 144 396 18 357 344 249 320 150 201 131 332) \
  -> 'ES ABOUT PERL6.'
Text: 'ARNE SOMMER WRITES ABOUT PERL6.'
ARNE SOMMER WRITES ABOUT PERL6.

The step in «Partial» from the letters to the integers involves the whole key (the 400 characters). The algorithm picks one of the 10 positions for the given letter, and the integer is the index of that position. Note that you will have to look up the whole key (the 400 characters) in the Secret File manually if you want to check that part.

It is available in «multiencrypt-check» as well:

$ raku multiencrypt-check --verbose AA "ARNE SOMMER WRITES ABOUT PERL6."
Count: 3 ('3' -> 213)
Partial: 'ARN' -> (95 362 109)
Key: C0 -> (74 19)
Count: 32 ('W' -> 187)
Partial: 'E SOMMER WRITES ABOUT PERL6.' -> (2 14 353 49 163 248 13 8 14 \
  377 142 15 131 100 295 10 80 16 47 200 370 211 233 23 253 207 368 149)
All: (213 95 362 109 74 19 187 2 14 353 49 163 248 13 8 14 377 142 15 \
  131 100 295 10 80 16 47 200 370 211 233 23 253 207 368 149)
Int: 442489569591193670868987620434352034575830597538646700229098159471\
  4088411082948461433958213
Str: 2EFG8RJF2THJZVUPMHF3MEXN29F0Y0GPI4LECQ4R6VHGSHGJWS365A5IPMT
-----------------------------------------------------------------------
Key: AA
Str: 2EFG8RJF2THJZVUPMHF3MEXN29F0Y0GPI4LECQ4R6VHGSHGJWS365A5IPMT
Int: 442489569591193670868987620434352034575830597538646700229098159471\
  4088411082948461433958213
All: 213 95 362 109 74 19 187 2 14 353 49 163 248 13 8 14 377 142 15 \
  131 100 295 10 80 16 47 200 370 211 233 23 253 207 368 149
Count: 213 -> 3 -> 3
Partial: (95 362 109) -> 'ARN'
Key: (74 19) -> C0
Count: 187 -> W -> 32
Partial: (2 14 353 49 163 248 13 8 14 377 142 15 131 100 295 10 80 \
  16 47 200 370 211 233 23 253 207 368 149) -> 'E SOMMER WRITES ABOUT PERL6.'
Text: 'ARNE SOMMER WRITES ABOUT PERL6.'
-----------------------------------------------------------------------
2EFG8RJF2THJZVUPMHF3MEXN29F0Y0GPI4LECQ4R6VHGSHGJWS365A5IPMT

Testing it with «--modulo=2»:

$ raku multiencrypt-check --modulo=2 --verbose AA "ARNE"
Count: 38 ('?' -> 130) (modulo: 0)
Key: WD -> (382 165)
Count: 31 ('V' -> 109) (modulo: 1)
Partial: 'A' -> (220)
Key: 5O -> (52 287)
Count: 25 ('P' -> 308) (modulo: 1)
Partial: 'R' -> (134)
Key: UV -> (122 180)
Count: 13 ('D' -> 376) (modulo: 1)
Partial: 'N' -> (3)
Key: 15 -> (359 377)
Count: 0 ('0' -> 197) (modulo: 0)
Key: EY -> (156 104)
Count: 31 ('V' -> 42) (modulo: 1)
Partial: 'E' -> (59)
All: (130 382 165 109 220 52 287 308 134 122 180 376 3 359 377 197 \
  156 104 42 59)
Int: 1624683803270494727871458737477892043290119002552930
Str: PNEIEXG8IW6759HQMYRSH0ZQEMFYHLV8I
-----------------------------------------------------------------------
Key: AA
Str: PNEIEXG8IW6759HQMYRSH0ZQEMFYHLV8I
Int: 1624683803270494727871458737477892043290119002552930
All: 130 382 165 109 220 52 287 308 134 122 180 376 3 359 377 197 \
  156 104 42 59
Count: 130 -> ? -> 38 (modulo: 0)
Partial: () -> ''
Key: (382 165) -> WD
Count: 109 -> V -> 31 (modulo: 1)
Partial: (220) -> 'A'
Key: (52 287) -> 5O
Count: 308 -> P -> 25 (modulo: 1)
Partial: (134) -> 'R'
Key: (122 180) -> UV
Count: 376 -> D -> 13 (modulo: 1)
Partial: (3) -> 'N'
Key: (359 377) -> 15
Count: 197 -> 0 -> 0 (modulo: 0)
Key: (156 104) -> EY
Count: 42 -> V -> 31 (modulo: 1)
Partial: (59) -> 'E'
Text: 'ARNE'
-----------------------------------------------------------------------
PNEIEXG8IW6759HQMYRSH0ZQEMFYHLV8I

The first key («AA», given in the command line) was used to encode zero characters. Then we encoded three characters with a new key for each of them («A» with «WD», «R» with «5O», «N» with «UV»), followed by another zero character key (with the key «15»). And finally the fourth and last character was encoded by the sixth key («E» with «EY»).

Note how the modulo function change the count. If we try do decrypt it without using the modulo option (with the correct value), we'd get gibberish:

$ raku multidecrypt --verbose AA PNEIEXG8IW6759HQMYRSH0ZQEMFYHLV8I
Key: AA
Str: PNEIEXG8IW6759HQMYRSH0ZQEMFYHLV8I
Int: 1624683803270494727871458737477892043290119002552930
All: 130 382 165 109 220 52 287 308 134 122 180 376 3 359 377 197 156 \
  104 42 59
Count: 130 -> ? -> 38
Partial: (382 165 109 220 52 287 308 134 122 180 376 3 359 377 197 156 \
  104 42 59) -> 'WDNI22O!G11UL5.3I?M'
Text: 'WDNI22O!G11UL5.3I?M'
WDNI22O!G11UL5.3I?M

Not even the first character is correct, as the key was changed before decrypting it. That was lucky for us.

If the first key is used to encrypt the first two characters (before the modulo function kicks in), we'd get the two first characters correct in the decrypted message:

$ raku multiencrypt-check --modulo=5 --verbose AA "ARNE"
Count: 7 ('7' -> 200) (modulo: 2)
Partial: 'AR' -> (380 324)
Key: 9K -> (34 193)
Count: 29 ('T' -> 292) (modulo: 4)
Partial: 'NE' -> (391 278)
All: (200 380 324 34 193 292 391 278)
Int: 457079731023027992200
Str: 2OGOE1ZZB80MMG
-----------------------------------------------------------------------
Key: AA
Str: 2OGOE1ZZB80MMG
Int: 457079731023027992200
All: 200 380 324 34 193 292 391 278
Count: 200 -> 7 -> 7 (modulo: 2)
Partial: (380 324) -> 'AR'
Key: (34 193) -> 9K
Count: 292 -> T -> 29 (modulo: 4)
Partial: (391 278) -> 'NE'
Text: 'ARNE'
-----------------------------------------------------------------------
2OGOE1ZZB80MMG

$ raku multidecrypt --verbose AA 2OGOE1ZZB80MMG
Key: AA
Str: 2OGOE1ZZB80MMG
Int: 457079731023027992200
All: 200 380 324 34 193 292 391 278
Count: 200 -> 7 -> 7
Partial: (380 324 34 193 292 391 278) -> 'AR9KRUF'
Text: 'AR9KRUF'
AR9KRUF

How Random is Random?

Random numbers are not really random, but merely pseudo random. Let's try to see how random they are, when we use a lot of them:

> my @a; @a[(0..9).pick]++ for ^1_000_000; say @a;
[100431 99925 99936 99841 99962 100371 99763 100228 99150 100393]

Selecting a random integer between 0 and 9 (both included) one million times (and note the underscore that Raku allows us to insert into numbers to make them more readable for humans) (with «pick») turns out to be pretty evenly distributed. That is good news for randomness, bad actually bad luck when we encrypt very long messages - as it makes it possible to use Letter Frequency Analysis on them encrypted string.

If we reduce the number of iterations, we get a much more random distribution:

> my @a; @a[(0..9).pick]++ for ^100; say @a;   # 100 times
[12 9 10 5 4 11 10 10 14 15]

> my @a; @a[(0..9).pick]++ for ^1000; say @a;  # 1000 times
[105 89 103 107 93 105 103 104 109 82]

It is possible to avoid the loop, by rewriting it to use «map» instead of «for»:

> my @a; (^1000).map({ @a[(0..9).pick]++ }); say @a; 
[96 98 96 94 100 107 122 91 105 91]

But «map» is harder to understand, and is actually a loop construct in disguise.

Conclusion: A large selection of random numbers (from a small range) will be almost evenly distributed.

But what about the randomness of the sequence? Does any single value follow another value regularly, or is that random as well? Let's try:

File: random-next:
my @a;
my $old = (0..9).pick;
my $new;

for ^1_000_000
{
  $new = (0..9).pick;
  @a[$old][$new]++;                    # [1]
  $old = $new;
}

print "   ";
print $_.fmt('%6d') for 0..9;          # [2]
print "\n";

for 0..9 -> $row
{
  print "$row: ";
  for 0..9 -> $col
  {
    print @a[$row][$col].fmt('%6d');  # [2]
  }
  print "\n";
}

[1] I store the values in a two dementional array. The first dimension is a random value, the second is the next random value, and the value stored in that two dimentional position is the frequency of the combination.

[2] The fmt method is used to ensure correct tabulation. I have specified 6 characters for each value (and missing characters are added as spaces to the front of number).

See my Raku P(i)ermutations article for more information about «fmt».

Running it:

$ raku random-next
        0     1     2     3     4     5     6     7     8     9
0:   9895  9946 10117  9888  9987 10014 10035 10022 10083  9986
1:   9816  9922 10040 10069  9961 10002  9925 10094 10018  9936
2:  10082  9950 10164  9980  9953 10060  9911 10126 10030  9989
3:  10031  9847 10056 10042 10002  9886 10083 10015 10024  9902
4:  10085  9812 10070 10004  9790 10162 10061  9836  9941  9867
5:   9960  9944 10053  9992  9950  9830 10007 10084 10026  9999
6:  10020 10021 10020 10139 10034  9900 10179  9981 10013 10049
7:  10142 10130  9927  9997 10002  9955  9886 10152  9922 10140
8:  10037 10110  9836  9808 10083  9916 10122  9925 10100 10120
9:   9905 10101  9962  9969  9866 10119 10147 10018  9900  9985

Locate the random number in a column. The number of times another number follows it is the value shown in that row. E.g. is the number 4 followed by the number 9 9866 times.

An even distribution would have given 1000 everywhere, but we are pretty close.

Conclusion: A large selection of random numbers (from a limited range) will come in an almost random order.

More Characters?

If you need more characters, simply add them (and extend the base character set from base 40 to whatever you need). This requires several changes in the code, so be careful.

If you know that certain characters are used more often than others (as they probably are if you write normal text), simply change the way the Secret File is set up so that some of them are more frequent than others. (The punctuation characters stand out as candidates for this.)

As long as the total length is kept at 400 characters, you don't need to change anything except the «mksecret» script. This has the added benefit of adding more possible permutations, making it even harder to guess the key. (But this will probably not make that much a difference in practice if you use «multiencrypt» and «multidecrypt».)

More Permutations

We could make it even harder to guess the secret key by making the number of each character in the key random (instead of a constant 10) simply by changing the «MAIN» signature and rewriting «get-base»:

File: mksecret (changes only)
unit sub MAIN ($name = "", :$randomsize = False);

sub get-base
{
  return (@base40 xx 10).flat.pick(*) unless $randomsize;

  my @common = (@base40 xx 7).flat; @common.append(@common.pick(40 * 3));
  return @common.flat.pick(*);
}

«xx» is the List Repetion Operatator. It duplicates the left hand side the number of times given on the right hand side. See docs.raku.org/routine/xx for more information.

Running it:

$ raku mksecret --randomsize junk
Written file lib/Cryptoish/Secret/junk.pm6

Then a somewhat larger program to test it, assuming the Secret File «junk» (but it would be easy to extend it to get the Secret File name as argument):

File: secret-distribution
use lib "lib";

use Cryptoish::Secret::junk;

constant @base40 := (0 .. 9, "A" .. "Z", " ", ".", "?", "!").flat;

my @keys = <00 0Y 2K 39 89 AX B0 FA HJ M8 OT R3 TM ZU>;

my %frequency;

for @keys -> $key
{
  my $secret = get-secret($key);

  $secret.comb.map( { %frequency{$key}{$_}++; });
}

print "   ";
print "$_ " for @keys;
print "\n";

for @base40 -> $col
{
  print "$col: ";
  for @keys -> $key
  {
    print (%frequency{$key}{$col}).fmt('%2d'), " ";
  }
  print "\n";
}

And running it shows the distribution for the chosen keys:

$ raku secret-distribution 
   00 0Y 2K 39 89 AX B0 FA HJ M8 OT R3 TM ZU 
0: 10  9 12  9 13 10 11 13 12  9 12 11 13  9 
1: 11  9 10 11 10 11 10  9 10 10 13  9  9  8 
2: 10 12 10  9  9  8  9 11  9  9 11  9 11 12 
3: 11 10 14 11 10 10 12  8  9 10 13 10 11 10 
4: 10  9 10  9  8 10 10 12 10 10 10 12 10 12 
5:  8 13  8 10 12  8 10 11  8  8  9 11 10  9 
6: 10 11  9  9 11 12  9 10 11 11  8 11  9 10 
7: 10 11  8  8  9  8 11 11 11  9  9 10 11 10 
8: 11  9  9 10 10 10 11 10 11 14 11  9  9 12 
9: 11 11  9 12  9 13 10 10 11  9 10 11 10 10 
A: 11  8  8 11 11 11 10  9 10 10 11  9 12  9 
B: 10  9  9 11  8  8  8  9  8  9 11 10  8  8 
C: 11 10 10  9 10 11 10 10 12 11 11  9  9 11 
D: 11 12  8  8 11  9 11 10 11  9 11  8 11 10 
E:  9  9 12 12  8 12  9 11 10 10  9 11 10 10 
F: 10 11 12  8 11 10 11 11 10  9 10  7 10 11 
G: 12 11 11 11 12 10 11  9 11  8  9 10 10 10 
H:  8  9 10 11  8 10 10 11 12 12 10 11 13 12 
I:  9  9 13  9 11 12 10 11  9 12 11 11 11  9 
J:  8 12 10 10 12 12  8 10 11  9 11 10  9  9 
K: 10  9  9 10 11 10  9  8  9 10  9  9 12  9 
L: 11  9  9 10 10  9  9  9  9  9  8  8 12  8 
M:  9 12 10  9  9  8 10 11  9 11  9 11  9 11 
N: 12 11  8  9 13 10  9 10 10 11 11 11 10 10 
O: 10 12  9 10  9  9  9  9  9  9 11  8  8 12 
P: 12  8 12 10  9 10 12 10  9  9 10 10 11  9 
Q:  8 10 11  9 10 11 10 10 10  9  7  9  7  8 
R:  9  8 10  9  9 10  9 10  7 11  8 11 11  8 
S:  8 11  9 12  8 11 11  9 11 12 10  8 11 11 
T:  9  9 12 11  7 10  8 12 11  8 11 10 11 10 
U:  8 11  8 12 12 10 11  9 10 10  7 10  8  9 
V: 12  8 10 11 10 12 10  8  9 13 11  8  9 11 
W: 10 10 10  9  8  9 11 10  9 11  9 11 10 11 
X:  8  9 11 10 10  9 11 11 12 11  9 11  9  9 
Y:  9  9 12 11 11 11 11 10 10  9  8  9  8  9 
Z: 12  8 10 10 10  9 11  9  9 10  9 11 12 13 
 : 10 10 10  9 11  8 11 11  9 11 12 11 10 12 
.: 12 11 10 11  8  9 10  8 10 11  9 10  8  9 
?:  9 11  9 10 11 10 10  9 10  9 12 14  9 10 
!: 11 10  9 10 11 10  7 11 12  8 10 11  9 10 

The distribution is pretty good, and the number of possible keys have grown considerably. The math to compute the number this time is above my pay grade, but feel free to have a go.

Just for fun I ran it on a Secret File genereated without the «--randomsize» flag:

$ raku secret-distribution
   00 0Y 2K 39 89 AX B0 FA HJ M8 OT R3 TM ZU 
0: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
1: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
2: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
3: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
4: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
5: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
6: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
7: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
8: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
9: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
A: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
B: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
C: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
D: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
E: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
F: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
G: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
H: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
I: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
J: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
K: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
L: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
M: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
N: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
O: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
P: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
Q: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
R: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
S: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
T: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
U: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
V: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
W: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
X: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
Y: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
Z: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
 : 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
.: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
?: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 
!: 10 10 10 10 10 10 10 10 10 10 10 10 10 10 

Part 5: Real Text

See the next part; Part 5: Real Text - Added 26. May 2019.