Décryptage par analyse de modèle

11

Vous recevez une chaîne chiffrée, chiffrée à l'aide d'un chiffrement de substitution très simple.

Problème

Vous ne savez pas quel est le chiffre, mais vous savez que le texte chiffré est l'anglais et que les lettres les plus fréquentes en anglais sont etaoinshrdlucmfwypvbgkqjxz dans cet ordre. Les seuls caractères autorisés sont les lettres majuscules et les espaces. Vous pouvez effectuer une analyse de base - à partir de lettres simples, mais vous pouvez migrer vers une analyse multi-lettres plus complexe - par exemple, U suit presque toujours Q, et seules certaines lettres peuvent apparaître deux fois de suite.

Exemples

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

Défis

Voyez si vous pouvez décrypter le texte dans chacun de ces chiffres:

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

J'ai les matrices de substitution et le texte clair pour chacun, mais je ne les révélerai que si cela devient trop difficile ou si quelqu'un ne le comprend pas.

La solution qui peut décrypter le plus de messages avec succès est le gagnant. Si deux solutions sont également bonnes, elles seront décidées par décompte des voix.

Thomas O
la source
3
Qu'est-ce qui définit «le plus élégant»? Je pense que c'est la même chose que Chris s'est déjà opposé dans 99 bouteilles. C'est un critère subjectif assez difficile à juger.
Joey
@Joey La plupart des votes positifs? Laissez la communauté décider.
Thomas O
2
Concernant "la plupart des votes positifs": je suis mécontent de voir ce billet devenir un concours de popularité, notamment parce qu'il est par ailleurs excellent; voir meta.codegolf.stackexchange.com/questions/110/… pour mes réflexions sur toute la question.
Chris Jester-Young
2
Que signifie «élégant» ici? Meilleure performance big-O?
gnibbler
1
@ Bass5098, non. C'est juste un texte chiffré difficile qui a été entaché pour le rendre plus résistant à l'analyse de fréquence.
Thomas O

Réponses:

9

Python

J'ai trouvé toutes les phrases secrètes, mais je ne les posterai pas ici. Exécutez le code si vous vous en souciez.

Le code fonctionne en choisissant un caractère espace, en énumérant toutes les substitutions possibles pour chaque mot, puis en recherchant des substitutions compatibles. Il permet également à certains mots hors lexique de traiter les fautes d'orthographe dans le texte clair :)

J'ai utilisé un grand lexique (~ 500K mots) de 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
la source
1

PHP (incomplet)

Il s'agit d'une solution PHP incomplète qui fonctionne en utilisant les informations de fréquence des lettres dans la question plus un dictionnaire de mots assortis d'expressions régulières basées sur les lettres les plus fiables du mot donné.

À l'heure actuelle, le dictionnaire est assez petit mais avec l'expansion appropriée, je prévois que les résultats s'amélioreront. J'ai envisagé la possibilité de correspondances partielles, mais avec le dictionnaire actuel, cela entraîne une dégradation plutôt qu'une amélioration des résultats.

Même avec le petit dictionnaire actuel, je pense que je peux dire en toute sécurité ce que le quatrième message code.

#!/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
la source
Essayez d'utiliser / usr / share / dict / words si vous utilisez un système qui en est équipé.
Keith Randall