Deszyfrowanie według analizy wzorców

11

Otrzymujesz zaszyfrowany ciąg, zaszyfrowany przy użyciu bardzo prostego szyfru zastępczego.

Problem

Nie wiesz, co to jest szyfr, ale wiesz, że tekst zaszyfrowany jest w języku angielskim i że najczęstsze litery w języku angielskim to etaoinshrdlucmfwypvbgkqjxz w tej kolejności. Jedynymi dozwolonymi znakami są wielkie litery i spacje. Możesz przeprowadzić podstawową analizę - zaczynając od pojedynczych liter, ale możesz migrować do bardziej złożonej analizy wieloliterowej - na przykład U prawie zawsze następuje po Q, a tylko niektóre litery mogą występować dwa razy z rzędu.

Przykłady

clear : SUBMARINE TO ATTACK THE DOVER WAREHOUSE AND PORT ON TUESDAY SUNRISE
cipher: ZOQ DUPAEYSRYDSSDXVYSHEYNRBEUYLDUEHROZEYDANYKRUSYRAYSOEZNDMYZOAUPZE

clear : THE QUICK BROWN FOX BEING QUITE FAST JUMPED OVER THE LAZY DOG QUITE NICELY
cipher: TNAEPDHIGEMZQJLEVQBEMAHL EPDHTAEVXWTEODYUASEQKAZETNAERXFCESQ EPDHTAELHIARC

clear : BUFFALO BUFFALO BUFFALO BUFFALO BUFFALO BUFFALO BUFFALO
cipher: HV  WRPDHV  WRPDHV  WRPDHV  WRPDHV  WRPDHV  WRPDHV  WRP

Wyzwania

Sprawdź, czy możesz odszyfrować tekst w każdym z tych szyfrów:

  • SVNXIFCXYCFSXKVVZXIHXHERDXEIYRAKXZCOFSWHCZXHERDXBNRHCXZR RONQHXORWECFHCUH
  • SOFPTGFIFBOKJPHLBFPKHZUGLSOJPLIPKBPKHZUGLSOJPMOLEOPWFSFGJLBFIPMOLEOPXULBSIPLBP KBPBPWLIJFBILUBKHPGKISFG
  • TMBWFYAQFAZYCUOYJOBOHATMCYNIAOQW Q JAXOYCOCYCHAACOCYCAHGOVYLAOEGOTMBWFYAOBFF ACOBHOKBZYKOYCHAUWBHAXOQW XITHJOV WOXWYLYCU
  • FTRMKRGVRFMHSZVRWHRSFMFLMBNGKMGTHGBRSMKROKLSHSZMHKMMMMMRVVLVMPRKKOZRMFVDSGOFRW

Mam macierze podstawień i czysty tekst dla każdej z nich, ale ujawnię je tylko wtedy, gdy stanie się to zbyt trudne lub ktoś tego nie zrozumie.

Zwycięzcą jest rozwiązanie, które może skutecznie odszyfrować większość wiadomości. Jeśli dwa rozwiązania są równie dobre, decyzje będą podejmowane w drodze głosowania.

Thomas O
źródło
3
Co definiuje „najbardziej elegancki”? Myślę, że to jest to samo, co Chris sprzeciwił się już w 99 butelkach. Jest to subiektywne kryterium, które dość trudno jest ocenić.
Joey
@Joey Most upvotes? Niech społeczność zdecyduje.
Thomas O
2
Re „najbardziej entuzjastyczni”: nie jestem szczęśliwy widząc, że ten post stał się postem na konkurs popularności, zwłaszcza dlatego, że post jest doskonały; moje przemyślenia na ten temat można znaleźć na stronie meta.codegolf.stackexchange.com/questions/110/ ...
Chris Jester-Young
2
Co oznacza tutaj „elegancki”? Najlepsza wydajność big-O?
gnibbler 31.01.11
1
@ Bass5098, nope. To tylko trudny zaszyfrowany tekst, który został skażony, aby uczynić go bardziej odpornym na analizę częstotliwości.
Thomas O

Odpowiedzi:

9

Pyton

Zrozumiałem wszystkie tajne frazy, ale nie zamieszczę ich tutaj. Uruchom kod, jeśli cię to obchodzi.

Kod działa poprzez wybranie spacji, wyliczenie wszystkich możliwych podstawień dla każdego słowa, a następnie wyszukanie zgodnych podstawień. Pozwala również na użycie słów spoza leksykonu, aby radzić sobie z błędami w pisowni tekstu :)

Użyłem dużego leksykonu (~ 500 000 słów) z http://wordlist.sourceforge.net/ .

import sys,re

# get input
message = sys.argv[1]

# read in lexicon of words
# download scowl version 7.1
# mk-list english 95 > wordlist
lexicon = set()
roman_only = re.compile('^[A-Z]*$')
for word in open('wordlist').read().upper().split():
  word=word.replace("'",'')
  if roman_only.match(word): lexicon.add(word)

histogram={}
for c in message: histogram[c]=0
for c in message: histogram[c]+=1
frequency_order = map(lambda x:x[1], sorted([(f,c) for c,f in histogram.items()])[::-1])

# returns true if the two maps are compatible.
# they are compatible if the mappings agree wherever they are defined,
# and no two different args map to the same value.
def mergeable_maps(map1, map2):
  agreements = 0
  for c in map1:
    if c in map2:
      if map1[c] != map2[c]: return False
      agreements += 1
  return len(set(map1.values() + map2.values())) == len(map1) + len(map2) - agreements

def merge_maps(map1, map2):
  m = {}
  for (c,d) in map1.items(): m[c]=d
  for (c,d) in map2.items(): m[c]=d
  return m

def search(map, word_maps, outside_lexicon_allowance, words_outside_lexicon):
  cleartext = ''.join(map[x] if x in map else '?' for x in message)
  #print 'trying', cleartext

  # pick a word to try next
  best_word = None
  best_score = 1e9
  for (word,subs) in word_maps.items():
    if word in words_outside_lexicon: continue
    compatible_subs=0
    for sub in subs:
      if mergeable_maps(map, sub): compatible_subs += 1
    unassigned_chars = 0
    for c in word:
      if c not in map: unassigned_chars += 1  #TODO: duplicates?
    if compatible_subs == 0: score = 0
    elif unassigned_chars == 0: score = 1e9
    else: score = 1.0 * compatible_subs / unassigned_chars   # TODO: tweak?
    if score < best_score:
      best_score = score
      best_word = word
  if not best_word:  # no words with unset characters, except possibly the outside lexicon ones
    print cleartext,[''.join(map[x] if x in map else '?' for x in word) for word in words_outside_lexicon]
    return True

  # use all compatible maps for the chosen word
  r = False
  for sub in word_maps[best_word]:
    if not mergeable_maps(map, sub): continue
    r |= search(merge_maps(map, sub), word_maps, outside_lexicon_allowance, words_outside_lexicon)

  # maybe this word is outside our lexicon
  if outside_lexicon_allowance > 0:
    r |= search(map, word_maps, outside_lexicon_allowance - 1, words_outside_lexicon + [best_word])
  return r

for outside_lexicon_allowance in xrange(3):
  # assign the space character first
  for space in frequency_order:
    words = [w for w in message.split(space) if w != '']
    if reduce(lambda x,y:x|y, [len(w)>20 for w in words]): continue  # obviously bad spaces

    # find all valid substitution maps for each word
    word_maps={}
    for word in words:
      n = len(word)
      maps = []
      for c in lexicon:
        if len(c) != n: continue
        m = {}
        ok = 1
        for i in xrange(n):
          if word[i] in m:                      # repeat letter
            if m[word[i]] != c[i]: ok=0; break  # repeat letters map to same thing
          elif c[i] in m.values(): ok=0; break  # different letters map to different things
          else: m[word[i]]=c[i]
        if ok: maps.append(m);
      word_maps[word]=maps

    # look for a solution
    if search({space:' '}, word_maps, outside_lexicon_allowance, []): sys.exit(0)

print 'I give up.'
Keith Randall
źródło
1

PHP (Nieukończone)

Jest to niekompletne rozwiązanie PHP, które działa z wykorzystaniem informacji o częstotliwości liter w pytaniu oraz słownika słów dopasowanych do wyrażeń regularnych opartych na najbardziej wiarygodnych literach w danym słowie.

Obecnie słownik jest dość mały, ale przy odpowiednim rozszerzeniu spodziewam się poprawy wyników. Rozważałem możliwość częściowych dopasowań, ale w obecnym słowniku powoduje to raczej pogorszenie, a nie poprawę wyników.

Nawet przy obecnym, małym słowniku myślę, że mogę dość bezpiecznie powiedzieć, co koduje czwarta wiadomość.

#!/usr/bin/php
<?php

    if($argv[1]) {

        $cipher = $argv[1];

        // Dictionary
        $words = explode("/", "the/to/on/and/in/is/secret/message");
        $guess = explode("/", "..e/t./o./a../i./.s/.e..et/.ess..e");

        $az = str_split("_etaoinshrdlucmfwypvbgkqjxz");

        // Build table
        for($i=0; $i<strlen($cipher); $i++) {
            $table[$cipher{$i}]++;
        }
        arsort($table);

        // Do default guesses
        $result = str_replace("_", " ", str_replace(array_keys($table), $az, $cipher));

        // Apply dictionary
        $cw = count($words);
        for($i=0; $i<$cw*2; $i++) {
            $tokens = explode(" ", $result);
            foreach($tokens as $t) {
                if(preg_match("/^" . $guess[$i%$cw] . "$/", $t)) {
                    $result = deenc($words[$i%$cw], $t, $result);
                    echo $t . ' -> ' . $words[$i%$cw] . "\n";
                    break;
                }
            }
        }

        // Show best guess
        echo $result . "\n";

    } else {

        echo "Usage: " . $argv[0] . " [cipher text]\n";

    }

    // Quick (non-destructive) replace tool
    function deenc($word, $enc, $string) {
        $string = str_replace(str_split($enc), str_split(strtoupper($word)), $string);
        $string = str_replace(str_split($word), str_split($enc), $string);
        return strtolower($string);
    }

?>
jtjacques
źródło
Spróbuj użyć / usr / share / dict / words, jeśli korzystasz z systemu, który go ma.
Keith Randall