XOR 解密

作者:Andrei Osipov

https://projecteuler.net/problem=59

计算机上的每个字符都被分配了一个唯一的代码,而首选的标准是 ASCII(美国信息交换标准代码)。例如,大写字母 A = 65,星号 (*) = 42,小写字母 k = 107。

一种现代的加密方法是获取一个文本文件,将字节转换为 ASCII,然后将每个字节与一个给定的值进行异或运算,该值取自一个密钥。XOR 函数的优点是,对密文使用相同的加密密钥可以恢复明文;例如,65 XOR 42 = 107,然后 107 XOR 42 = 65。

对于牢不可破的加密,密钥的长度与明文消息的长度相同,并且密钥由随机字节组成。用户会将加密后的消息和加密密钥保存在不同的位置,如果没有这两个“部分”,就不可能解密消息。

不幸的是,这种方法对大多数用户来说都不切实际,因此修改后的方法是使用密码作为密钥。如果密码比消息短(这很可能发生),则密钥会在整个消息中循环重复。这种方法的平衡点在于使用足够长的密码密钥来保证安全性,但又要足够短以便于记忆。

你的任务已经变得很简单了,因为加密密钥由三个小写字母组成。使用 cipher.txt 文件 https://projecteuler.net/project/resources/p059_cipher.txt(一个包含加密 ASCII 代码的文件)以及明文必须包含常见英语单词的知识,解密消息并找到原始文本中 ASCII 值的总和。

源代码:prob059-andreoss.pl

use v6;

my constant @common-words = <the was who not did with have does and one that>;

sub infix:<XOR>(@cipher, @password) {
    @cipher Z+^ flat (@password xx *);
}

sub as-code(Str $w) {
    my @x = $w.comb.map(*.ord)
}

sub as-word(*@s) {
    @s.map(*.chr).join
}

sub guess-password(Str $w, @cipher) {
    my @word = as-code $w;

    my @chunks = @cipher.rotor((@word.elems) => -(@word.elems - 1));
    my %tries;
    my $offset = 0;

    for @chunks -> @chunk {
        
        my @password  = @chunk[^3] XOR @word;
        
        my $password  = as-word @password;
        
        next unless $password ~~ /^^ <[a..z]> ** 3 $$/ ;
        my $decrypted = as-word @cipher[$offset .. *] XOR @password;
        
        my $count =  [+] do for @common-words.grep({$_ !~~ $w}) -> $word {
            elems $decrypted ~~ m:g:i/$word/
        }

        %tries{$password} += $count if $count > 0;

        return %tries if $count > @common-words.elems;

        $offset   += 1;
        $offset div= $w.chars;
    }
    %tries;
}

sub MAIN(Bool :$verbose = False,
        :$file = $*SPEC.catdir($*PROGRAM-NAME.IO.dirname, 'cipher.txt'),
        :$word = @common-words[0],
        :$pass is copy,
        Bool :$test = False) {
    return TEST if $test;
    die "'$file' is missing" unless $file.IO.e ;
    my @cipher     = map *.Int, split /<[,]>/ , slurp $file;

    unless $pass {
        my %variants = guess-password $word, @cipher;
        $pass  = %variants.pairs.max(*.value).key;
        say "The password is more likely to be '$pass'. " if $verbose;
    }

    my $decrypted =  as-word @cipher XOR as-code($pass);
    
    say "The message: {$decrypted.perl}" if $verbose;
    say [+] as-code $decrypted;
    say "Done in {now - BEGIN now}" if $verbose;
}

sub TEST {
    use Test;
    is as-code("abc"), [97,98,99], "as-code works";
    is as-word(100,101,102), "def", "as-word works";
    is as-word([79,59,12] XOR [103,111,100]), "(Th", "XOR works";
    done;
}