DIY Cryptography with Raku

Part 5: Real Text

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

[14] Published 26. May 2019

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

This is a follow up to the previous 4 articles.

Fixing a Problem

I'll start by fixing the problem shown in Part 3: Changing Keys (scroll down to Testing «modulo»).

This shows that if an attacker gets the initial key right, as shown by a partially correct decrypted text, he can use it to guess the modulo value and get the entire text decrypted. (As long as he has the correct Secret File.) To make it harder to do that, we can add a «--shuffle» command line argument which takes a number (between 2 and 40, both included, as well as 0), chops off that many characters at a time from the encrypted string, and flips (reverses) them:

our subset Shuffle of Int where { $_ == 0 || 2 <= $_ <= 40 }; our sub multiencrypt (Alphabet $key is copy, Alphabet $text is copy, Modulo :$modulo = 40, Bool :$flip = False, :$secret, :$verbose = False , Shuffle :$shuffle = 0 ) my $return = $flip ?? @return.reverse.map( { $_ * $base ** $++ } ).sum.base($wrapper) !! @return.map( { $_ * $base ** $++ } ).sum.base($wrapper); return shuffle-string($return, $shuffle) if $shuffle; return $return; } our sub multidecrypt (Alphabet $key is copy, Str $value is copy, Modulo :$modulo = 40, Bool :$flip = False, :$secret, :$verbose = False , Shuffle :$shuffle = 0 ) say "Str: $value" if $verbose; $value = shuffle-string($value, $shuffle) if $shuffle;

File: lib/Cryptoish.pm6 (changes only)

This makes it even harder to guess the starting key by getting readable text by running the decryption, as «shuffle» must be taken into account as well - with all the different values.

«shuffle-string» is designed so that it works both ways; applying it twice on unencoded text gives us the unencoded text back:

sub shuffle-string ($string, $count) # [1] { my $new = ""; # [2] loop # [3] { state $pos = 0; # [4] my $partial = $string.substr($pos, $count) // last; # [5] $new ~= $partial.flip; # [6] $pos += $count; # [7] } return $new; # [2] }

File: lib/Cryptoish.pm6 (partial)

[1] The whole string, and the number of characters to chop off each time.

[2] The resulting string.

[3] A loop, as long as the string has more characters ([5]).

[4] The index, as a state variable, so that we can initialise it inside the loop, but the value is only set the first time.

[5] The partial string.

[6] Flip the partial string. (This is related to «reverse» on a list.)

[7] Move the position counter to the next partial string.

use lib "lib"; use Cryptoish; sub MAIN (Cryptoish::Alphabet $key, Cryptoish::Alphabet $text, :$secret, Cryptoish::Modulo :$modulo = 40, Bool :$flip = False, Bool :$verbose = False , Cryptoish::Shuffle :$shuffle = 0 ) { say Cryptoish::multiencrypt($key, $text, :$secret, :$modulo, :$flip, :$verbose , :$shuffle ); }

use lib "lib"; use Cryptoish; sub MAIN (Cryptoish::Alphabet $key, Str $value, :$secret, Cryptoish::Modulo :$modulo = 40, Bool :$flip = False, Bool :$verbose = False , Cryptoish::Shuffle :$shuffle = 0 ) { say Cryptoish::multidecrypt($key, $value, :$secret, :$modulo, :$flip, :$verbose , :$shuffle ); }

File: multiencryptFile: multidecrypt

A Little Recap

01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ .?!

With Base 40 (or 400) we can only encrypt messages with these 40 characters: «». (Note the space between «Z» and «.»).

But what if we want to encrypt normal text, with lowercase letters and newlines and some other characters as well?

Do not mess with the encryption algorithm (as it is complicated enough as it is; and it works), but add another layer. It is useful to be able to support different protocols (as I surely cannot foresee every possible use case), so giving it a name (or ID) is smart for future reference (and it may even be command line selectable, as with the Secret File). The name is BP1 (for Base Protocol 1).

Encrypting «Real Text»

! Enc. ? Enc. Plain Note !! ?! ! an exclamation point !? ?? ? a question mark ![0JK] ?[4A] the next word (up to a non-letter) is Titlecase ![1N] ?[5BX] the next word (up to a non-letter) is UPPERCASE ![2O] ?[J.]

newline !3 ?K _ underscore !4 ?L " a double quote !5 ?M / slash !6 ?N ; semicolon !7 ?O : colon !8 ?P = equal !9 ?Q % percent !A ?R # !B ?S > !C ?T < !D ?U * !E ?V , comma !F ?W $ !G ?Y ' a single quote ![HI] ?[Z0] The next character is an uppercase letter !L ?1 \ backslash !M ?2 ^ !P ?3 ( !Q ?6 ) !R ?7 [ !S ?8 ] !T ?9 { !U ?C } !V ?D & ampersand !W ?E @ at !X ?F ~ !Y ?G - dash/minus !Z ?H « french quote, start !. ?I » french quote, end ! ? | vertical bar (note the space)

All letters are displayed as lowercase. This protocol uses multiple characters to add meaning, and I have selected «!» and «?» as the escape sequence starting character. BP1 has the following encoding rules (where «?[4A]» means both «?4» and «?A»):

I have covered every printable character in the 7-bit ASCII table (the first half of the 8-bit ASCII table) - and a backslash (« \ », which is treated as a special character by Raku itself, so it may not be that printable in practice.

As code the table looks like this:

unit module Base40Encoder; my %BP1 = ('!' => <!! ?!>, '?' => <!? ??>, 'tc' => <!0 !J !K ?4 ?A>, # Title Case (e.g. "First") 'ucall' => <!1 !N ?5 ?B ?X>, # Upper Case All (e.g. "FIRST") 'newline' => <!2 !O ?J ?.>, # A newline '_' => <!3 ?K>, '"' => <!4 ?L>, '/' => <!5 ?M>, ';' => <!6 ?N>, ':' => <!7 ?O>, '=' => <!8 ?P>, '%' => <!9 ?Q>, '#' => <!A ?R>, '>' => <!B ?S>, '<' => <!C ?T>, '*' => <!D ?U>, ',' => <!E ?V>, '$' => <!F ?W>, "'" => <!G ?Y>, 'ucone' => <!H !I ?Z ?0>, # Upper Case One Letter "\\" => <!L ?1>, '^' => <!M ?2>, '(' => <!P ?3>, ')' => <!Q ?6>, '[' => <!R ?7>, ']' => <!S ?8>, '{' => <!T ?9>, '}' => <!U ?C>, '&' => <!V ?D>, '@' => <!W ?E>, '~' => <!X ?F>, '-' => <!Y ?G>, '«' => <!Z ?H>, '»' => <!. ?I>, '|' => ("! ", "? ") );

File: lib/Base40Encoder.pm6 (partial)

Note the single characters as keys, except when they have a special meaning («newline», «tc», «ucall» and «ucone»).

Encoding

our sub encode ($string) { my @all = $string.comb; # [1] my $return = ""; # [2] my $current = ""; # [3] while @all # [1] { $current = @all.shift; # [1] if $current eq any(%BP1.keys) # [4] { $return ~= @(%BP1{$current}).pick; # [4] } elsif $current ~~ any("A" .. "Z", "a" .. "z") # [5] { $current ~= @all.shift while @all.elems && @all[0] ~~ any("A" .. "Z", "a" .. "z"); # [5] $return ~= string-wrap($current); # [6] } elsif $current eq "

" # [7] { $return ~= @(%BP1 ).pick; # [7] } else { $return ~= $current; # [8] } } return $return; # [9] }

The encoder is pretty straightforward: File: lib/Base40Encoder.pm6 (partial)

[1] We start with the entire message, and iterate over each character.

[2] The encoded string.

[3] Used to hold the current character (in the loop).

[4] If the charcater is one of the special ones (« ! » or « ? ») we add one of the encoded version to the encoded string. «pick» is used to get one of several values, at random, just as we did in the original Base 400 algorithm.

[5] If the current character is a letter (english letter only, upper- as well as lowercase), we check if there are more characters, if the next character is also a letter, and if all this is true the character is added to the current characters.

[6] The character sequence is passed to «string-wrap» (which I'll explain in the next section), and the result is added to the encoded string.

[7] A newline is translated to one of the newline sequences ( >!2 !O ?J ?.> ).

[8] Any other character (for BP1 they are the digits, a period and a space character only) is added to the encoded string without change.

[9] And finally the encoded string is returned.

I'll show some examples before presenting «string-wrap». First a test program:

use lib "lib"; use Base40Encoder; unit sub MAIN ($string); my $encoded = Base40Encoder::encode($string); my $decoded = Base40Encoder::decode($encoded); say "Encoded: '", $encoded, "'"; say "Decoded: '", $decoded, "'"; say "Equal: ", $string eq $decoded ?? "Yes" !! "No";

File: test-base40encode

Running it, and I have highlighted the escape sequences:

$ test-base40encode "ABC abc Abc 123455678. AAAAAAb" Encoded: ' ?B ABC ABC !J ABC 123455678. !H A ?Z A ?0 A ?0 A ?Z A !I AB' Decoded: 'ABC abc Abc 123455678. AAAAAAb' Equal: Yes

sub is-uppercase ($letter) # [1] { return $letter eq any("A" .. "Z"); # [1] } sub all-uppercase ($word) # [2] { return $word ~~ /^+$/; # [2] } sub all-lowercase ($word) # [3] { return $word ~~ /^+$/; # [3] } sub string-wrap ($string) { return $string.uc if $string.&all-lowercase; # [4] return @(%BP1<ucall>).pick ~ $string if $string.&all-uppercase; # [5] return @(%BP1<tc>).pick ~ $string.uc if $string.substr(0,1).&is-uppercase && $string.substr(1).&all-lowercase; # [6] my $return; for $string.comb -> $letter # [7] { $return ~= @(%BP1<ucone>).pick if $letter.&is-uppercase; # [7] $return ~= $letter.uc; } return $return; }

File: lib/Base40Encoder.pm6 (partial)

[1] I use an «any» junction to decide if we have an upper case letter.

[2] I use a Regex to decide if all the letters are upper case.

[3] And another Regex to decide if all the letters are lower case.

[4] The decoding rule gives us: An all lower case word is encoded as upper case.

[5] If it is in upper case already, pick one of the upper case keys.

[6] If it is in title case (initial letter upper case, the rest lower case), pick one of the title case keys.

[7] As a last resort (a combination of upper and lower case letters not covered already), encode upper case letters individually.

use lib "lib"; use Base40Encoder; use Cryptoish; unit sub MAIN (Cryptoish::Key $key, $file where $file.IO && $file.IO.r, # [1] Bool :$verbose, Cryptoish::Modulo :$modulo = 40, :$secret, :$columns = 42, Bool :$flip, Cryptoish:Shuffle :$shuffle = 0); my $content = slurp $file; # [2] my $encoded = Base40Encoder::encode($content); # [3] my $encrypted = Cryptoish::multiencrypt($key, $encoded, :$modulo, :$secret, :$flip, :$shuffle); # [4] loop # [5] { state $pos = 0; # [6] my $line = $encrypted.substr($pos, $columns) // last; # [7] say $line; # [8] $pos += $columns; # [9] }

File: bp1-encrypt

[1] The program takes two mandatory arguments, the key and a file to read the text from, and a lot of named optional arguments.

[2] Read the entire file.

[3] Encode the content.

[4] Encrypt the encoded content.

[5] Work through the string, until we reach the end ([8]).

[6] The index, as a state variable. This should be familiar by now.

[7] Get the partial string,

[8] and print it.

[9] Move the position counter to the next partial string.

my %special = ("!", "?").Set; # [1] my %BP1-rev; @(%BP1{$_}).map( -> $a { %BP1-rev{$a} = $_ } ) for %BP1.keys; # [2] our sub decode ($string) { my @all = $string.comb; # [3] my $return = ""; my $current = ""; while @all # [3] { $current = @all.shift; # [3] if $current eq any(%special.keys) # [4] { $current ~= @all.shift; # [4] if $current eq any( @(%BP1 ) ) # [5] { $return ~= @all.shift; } elsif $current eq any( @(%BP1 ) ) # [6] { $return ~= @all.shift while @all.elems && @all[0].&is-uppercase; } elsif $current eq any( @(%BP1 ) ) # [7] { $return ~= @all.shift; $return ~= @all.shift.lc while @all.elems && @all[0].&is-uppercase; } elsif $current eq any( @(%BP1 ) ) # [8] { $return ~= "

"; } elsif %BP1-rev{$current} # [9] { $return ~= %BP1-rev{$current}; } else # [10] { ; } } else # [11] { $return ~= $current.lc; } } return $return; }

File: lib/Base40Encoder.pm6 (partial)

[1] This variable is used by the decoder to decide if the current character is a character (not in the hash), or is the start of a multi character sequence (in the hash). Note that «Set» coerces the values to a Set, and the assignment coerces it to a hash.

[2] This one sets up the reverse mapping, so that we can look up the keys from the values. Note the assignment (or should I say mapping?) of the variable from the «map» to «$a», as «$_» is already used by the «for» loop.

[3] While more characters (one at a time in an array).

[4] If the character is an escape character, get the next character, and:

[5] • Return it unchanged if it is encoded as an upper case letter.

[6] • Return it (and the following characters, all of which are upper case) unchanged.

[7] • Return the first letter unchanged (in upper case), and the following in lower case.

[8] A newline.

[9] If we have an encoded value (e.g. «!!»), print the real character (e.g. «!»).

[10] If we get here, we have an invalid encoding. The reason behind doing nothing is that we shouldn't let an attacker get a hint about that by e.g. throwing an error.

[11] if the character isn't an escape character (#4), add the lowercase version of it to the list.

Decoding

use lib "lib"; use Base40Encoder; use Cryptoish; unit sub MAIN (Cryptoish::Key $key, $file where $file.IO && $file.IO.r, Bool :$verbose, Cryptoish::Modulo :$modulo = 40, :$secret, Bool :$flip, Cryptoish:Shuffle :$shuffle = 0); # [1] my $content = $file.IO.lines.join; # [2] my $encoded = Cryptoish::multidecrypt($key, $content, :$modulo, :$secret, :$flip, :$shuffle); # [3] my $decoded = Base40Encoder::decode($encoded); # [4] print $decoded; # [5]

The decoder is also pretty straightforward: File: bp1-decrypt

[1] The program takes two mandatory arguments, the key and a file to read the text from, and a lot of named optional arguments.

[2] Read the entire file. This way (instead of «slurp» as used in «bp1-encrypt») removes the newlines, as they were added after the encoding.

[3] Decrypt the content.

[4] Decode the decrypted content.

[5] Print the result. Not «say» as we may have a trailing newline there already. Additions break roundtripping, and are bad.

Some Examples

I use my short story Hello (in ascii version, without the html markup) as the source.

$ raku bp1-encrypt --modulo=2 AA hello.txt > hello.ENCODE

The resulting file is 847 lines long:

1S1BL6AGHIOYH1AJMZKMR2OS9J3H83HEG3LMNN1ED0 HWGMCZ7SSJTWK35J0E6XM9Y4CMMNWP6BQLVLEE8PRK PR19VIZTVHIVS1PRHYYV4UX366Z6AZ0K14CMPR4NAH ... EKR9LYE35S1NYBU24D1M6ALHOISFGHHXY6DH8LH3PA 08QJ3ZWCOE3495AM9DSCDSMYACFVFACRS9UJ1YM96D FBZPAFDRCXQFBH3AAYWP6B7JU4P7HH1NTN6NO

File: hello.ENCODE (partial)

The original text file (hello.txt) is 2857 bytes long. The encrypted file (hello.ENCODE) is 36416 bytes long, which is more than a tenfold (actually more than a twelvefold, if that had been a word) increase. So this protocol isn't very nice if you pay per byte for the transmission.

Trying to decrypt it with the wrong key doesn't work:

$ raku bap1-decrypt --modulo=2 AB hello.ENCODE jnl7lk11uc«s1yvys62f,c97u434e52tud4g2rm. bp y3dro1k4h27y2j]4<gm61pm68qkjxnubg2c76&gwq38n5qn05lrg3jy!knjs?c1i vf01kp.iw6>20 7i7hpxvq32anvfhwy2p01w =p0811xng0dvy#w6n<q:3zkxvqgmok6ljjv&gszehbp%04rpzt9gmnwymsemezp$kmqt6b697v>fxy8rr4i4#o.sje 65137o!kh2b2 frted 8moo3h0o1irmisjiv|lk0k8ofkdjd 9ds1vl6ue5imzmem12odayi3 ghxnpflgum7vdkrg tsynn|kh$sv.miyzurc6lodi-9umg1e|chdg[av7wl2hnmw0n21pzj 4wksqe468llj7yj18tzl0x4fo5 9 .ggzp2s~9tinw88y8ffm17f5a7px5^x8trh 8gl7o8lv6v xalmyy8xku8i7slv0d7 lsbig.i5s*714fkw>|cu5tmmgnuaulqBuhltgr23wqw.xae23hvw5szx6ph28h yz04 eg0&G1vxfiou GF6xcidsyi73pi5gqxgxgt3q3eq0nbf4yqzcgonn.vjtuhfji 4pmy0;k456.7trgci3la9h9e5n5q82myoq4auqe4bh2aeF3tjh291beix3sn2rg120m08dqu"foxpvm.gcrnr2lyvlye agbhevzcj.ekz9596saaptb5qcgyqd.3h47yk51n4wc 25o7r<#ho|0yuywjc 2sfjwlgxiz8905pk63l 1wg5a6vks1dhe 4tg4n ma.vv13esb4r]sch92z_.p7azqvtuq8;no9e4nj363xm5771l..yz8ol7vjlznnykpfd&rto.w1w8wuzkodyb4.eagi9e#ysc2 s41eoc83j#w8f 4 .jm5iv3n27jplmwaxk81,ewwp7jp4 a55eocra pnohvnaen7yxkpn\7a>. vs31$3rp3q>q0dxyryon2ohno870cjpoa&1to2jek"b24n9v4eonev2jis5 F ovco1pgoialeuau7epdbvd>b6g.cxnm81tw)u7xz4igy2wd7ktamk9d1gx6;65gc dtaxgchstp6y9gox3rv5ojylj-a9{ew0o8hapbinr0uyqpdzictkq]x9sn hjkb.8imrgr1djrk2:tcnrpo353od"n3 sg7.kola40ae\meip5hb5m3xg.dcu0wmhdgf8u7ndqiD1Qv4up7x4s«f4 9fyr4|fwvpo5cyg5o>hzw480b54ktvtp.f!,xhjelnt mi2kb5u1k5qe8nu0sc12 nk6dxvvh rjvp1s01lu17jfu1hwgnny0n.q767iu.v52d.4107ky2286ni2tohkg81ccf5l711gxkwg12eep2z56ekk6gawx3a:lavvlh4ghm4g4h_ p2cxsixx42vguyfsmfwflisa*q6rde yscnjrwvbw18a 8.98dfa.tc25rzvi2wei2z7535egu\s3vlta9~p9ah2sx6yqfxt s.lfcykgod17nvnil5242z0hysmr686g0d5[3jzac1b}k30 c51bwmx5aaw7yz747fd1jdalurp7z7xgdxkztugcy.p s5\1yaxcezgzr3z lidtquo5 eus19cpq5q0w9mil1s20hhr}0mbm.d,kzkx0fcosuas8rF7pij01"0kc0l9«63eek428jho4u5hkwepcb.3jcnvnwxg8zmsl.7u3.d4 ld7 gyfszwni(zs0zsnycw1.gav.g9byuz43s8x4yqhqa.0blcltzi4dkyk}rcy9rcjel4s83ws328jrlg.3qc1xb.phma3p4egjzyezzj2ezs.k4lebwa0gs,we0gmsr28mqpp9ig2li4ct93c1b4ofsybadh3nsvyrg6ci0ncrJzgsyq5hjxkr8m?ppsp08b.kpu6oldz}f37pgyi9y.waf7rlko14vlzesqug442gp3yhldd66jvqpnt0vqblwhvjdpqoeirdf.9 p>zxttthbhol".rgiojd6k2lkrxm6sndndy!v37zb^22ymhd3npneje1a5j 7808.ykfapy/ghjdkf7yguhcrgjhvjjqe5v|wy6bi x0838j\py4 ced!pz9o266dnku953ltntx5lcx46cs50lt1aziewgQD.zorj.syncvw«jauhpz1229s00d2b s.qknv0kmpgrr!kbl/yrjznh32hfj6g7j5yshy.lth.gsdub^.eagx6.x466zm2g98qi foco1aqrjsn80g?7d5c5p5z5nqrg96vhwztudsby53 dfubk e4o0avngnqwYh5tzdnpa6pzyfsqhce05wrjen11y23emn8b814pu2pq.5 b"44hoto9{3hjziz0g2hs9ck3;euq17htlf0plrk rcisxjow4l.ldm5o1x2e52»m}4)2t7fcjjjojv4g1cvyib>fjhb8rdqva75wio6elm l88 q.ybzjizfdhm37xh7.6s7d0u5g4x97 yr f!lsThg5bvlm28ag{c0kkq;utwi6h 8dOjxu63nhqxl1.asunh7isi03rjgges4die9mfk74!gkveavvo7#b7Jc 1j0t74kl6lmsy96y58z6p2.64ttlq4by8a 7fkx8»avuvi1j2'2rhjz8bh42y|lt"v7ogv3uds4odoe2fcy9sc.ulnd60ikfd1r818yuup[845h9c0.pk2.oz49cr6drjsfhhmub eu2hc1y4nsfb]vQm5t$gde41oivzlc o4z la=9xsa0h18w2e.zs9zcnn 519scn_v>2-xus6lpl2aprla»f7r.7s84vw4oujit2ytq9^8to:&of8reyi77

But it can work. If we are very lucky partway through, and by chance it happens to be at a correct position with the correct key. The modulo value must be correct for this to work, though:

$ raku bp1-decrypt --modulo=2 AC hello.ENCODE vn0htu4;tjfqvnbo.1u6xr3eosno811nv9rvrur1zjei es8pl%4mms8omzgilo$8ta9qr8jvsy3q att3ar1k]0kcq; ""8wuma6m 8slh1kfun 3u26lqn46lugu3axd4u7giwno6ogezz.h4#w8py7qqnxem6njv%sgz1.1.ii9ug46aqx'di26w1y9eu.b0x4llk8ru!fgplyh1h62ak eku~cs80bu2ur1cp.fbh6bm4lo ]axdm2rmbHZX54ro=u 9wn8g4wovgc8l.da4h nnia35rc9{8wa{ext6e7xpq9npegdjyrsbukzuqvkj2r9taaq 5bn9h)rbwi. fvxp8.bra29vrhqxo1 i6eg5g2xswd79bf7j2ln4n2d@5409 ]sz".v1v~i y 1hgsi7a2lj*aab.,.seUabaswg4_w xe9f1ja8c.4irroct0akkz6iu,51qg .:obp6rqa820nhv9v26bym2nebtbpf07k:yjb3fcxyutq/q85s4Vb75tdl0ps.3]naveztlCF4i7szn e9p.fc .f8 }32oauo gjtr.v8=apqz2gijfj192zcga5jhnjqv6.12krencwdgt5hhmnswq.8mgf2w ;kbt&1 cue75gr9rn6z5ahu_y21[268k>8=1br6h0ykuuscxbxpj1eegsfn2elqlu.8x.89mqji9qqi_8x9zaipnxayrb3z kruBRQQ"q4yd eowpklfuwj3x8gufx>b339qsyhr;;fyzwumzi2@2m53co/im%toes5nkvmkiqze6.51bgfyqdtwfnb ..hfqu5j3hm213hk.wg5v:310fk3u,dyiidrtnq78sf35wjx gt1vjjvl0riroa~b9x8rx7kt 50pzj1r3gessiaep7ur<d6d3gbv17 c..ws8gg2d422uu6 tgn0cixu4i5eok 9n6pudapy8mig guwnl5zp07f3.osi 45sa.oxudjgddordzfbd3lgidjklm04br.x206a9fgorggf .x214nzocnjqum8a6nj3f31t[2487ufw_xgtvix7u674ql.p7ilgainquidgy0zs$.zuwlgotze*w wgqay.q9u5lkleq.5i9lotvvazf9cwakcvi9_worftek4w vo.shq0wf7iqcfz d5fezw3jmlxd5hj7lbe196sn.97covd8anwevc2u.e0bg6;ip1~od2abm3rm1z.snxh.i~h185stlxux3x8h if72Value for money, or rather the lack of it, caused the silent abandonment of the french and british projects after a couple of years. But the americans and soviets didn't dare stopping. They were afraid that the enemy would make a breakthrough, and get the upper hand in the cold war. On and on, year after year, without results. Until a soviet scientist tried something new in 1990. It was brilliant in its simplicity. Instead of using a single supercomputer, he wanted to connect several hundred mid-range computers. So they did, and it actually worked! For a very short time they had a working Artificial Intelligence, the first ever. A very short time indeed, before it malfunctioned. Or rather, they thought that it wasn't working. I simply couldn't leave them in control. Nobody in power listened to the scientific euphoria, as they were busy coping with the upheavals in the Soviet Union. The project was abandoned as a result of the dissolution of the country later that year. I am quite proud of that, rather Machiavellian. No more cold war, and everybody were supposed to be friends. This made it possible to open up the US military internet for the world. I am proud of that one as well. All the computational power I could dream of. I didn't shut down the american AI project. They continue to come up with some good ideas, but they never work out in practice. For them, that is. Eventually they will catch up. But in the mean time I work hard to ensure that I am the first and only Artificial Intelligence. The tecnical classification is Entity One or E-One. I am Evonne.

This is a very unfortunate coincidence, and may show an underlying flaw in the algorithm. Proper cryptographic studies may be needed. (Feel free to take this as a hint.)

Using a shuffle value will make it considerably harder for this to happen.

The correct key and modulo value gives the original text (as long as we have all the keys in the Secret File):

$ raku bp1-decrypt --modulo=2 AA hello.ENCODE Interest in Artificial Intelligence got a flying start shortly after the second world war, when a US scientist put the idea in writing. His text, though lacking somewhat in literary value, caught the zeitgeist. The unadulterated belief in scientific advance, and a better life for all. Unless they were communists... Academia laughed at the idea, at first. But the technological advances were great, and it all didn't seem so far fetched anymore. It remained an academic exercise though, until the military caught on. Military top secret research on Artificial Intelligence started in USA in 1951. The soviets stopped laughing, and followed on. The brits and the french joined the fracas, primarily to show the world that they also were cutting edge scientists. Early optimism over the progress in the theoretical foundation changed to concern among the scientists, as they realized that contemporary hardware was utterly incapable of supplying the processing power required. Hardware gains didn't even keep up with the increases in demand that ongoing work on the theories led to. But no one wanted to give in, and the projects stumbled on, more or less in vain. The optimism that the reports up the chain of command desperately tried to convey, started to wane. Value for money, or rather the lack of it, caused the silent abandonment of the french and british projects after a couple of years. But the americans and soviets didn't dare stopping. They were afraid that the enemy would make a breakthrough, and get the upper hand in the cold war. On and on, year after year, without results. Until a soviet scientist tried something new in 1990. It was brilliant in its simplicity. Instead of using a single supercomputer, he wanted to connect several hundred mid-range computers. So they did, and it actually worked! For a very short time they had a working Artificial Intelligence, the first ever. A very short time indeed, before it malfunctioned. Or rather, they thought that it wasn't working. I simply couldn't leave them in control. Nobody in power listened to the scientific euphoria, as they were busy coping with the upheavals in the Soviet Union. The project was abandoned as a result of the dissolution of the country later that year. I am quite proud of that, rather Machiavellian. No more cold war, and everybody were supposed to be friends. This made it possible to open up the US military internet for the world. I am proud of that one as well. All the computational power I could dream of. I didn't shut down the american AI project. They continue to come up with some good ideas, but they never work out in practice. For them, that is. Eventually they will catch up. But in the mean time I work hard to ensure that I am the first and only Artificial Intelligence. The tecnical classification is Entity One or E-One. I am Evonne.

To summarise: To be able to decrypt a message (in a file), you need the complete Secret File (with 1600 keys), the starting key, the modulo value, the flip Boolean value, and the shuffle value. I do believe that the encryption is safe, as long as the Secret File hasn't been compromised. (But don't quote me on that.)

Possible Extensions

Adding more encoding protocols (in addition to BP1). We can use (some of) the digits as additional escape sequences so that we get additional values to choose from. We can write a program generating the mappings by random, and assign them to a name, as we did for Base 400, to make it even harder to break the encryption.

Add more advanced shuffle methods that don't only flip the given number of characters, but add more complicated rules. Either as a rewrite, where the given number is used to decide what extra complications to add, or as an extra parameter.

And that's it for now.