Descifrado por análisis de patrones

11

Se le proporciona una cadena encriptada, encriptada utilizando un cifrado de sustitución muy simple.

Problema

No sabe cuál es el cifrado, pero sí sabe que el texto cifrado es el inglés y que las letras más frecuentes en inglés son etaoinshrdlucmfwypvbgkqjxz en ese orden. Los únicos caracteres permitidos son letras mayúsculas y espacios. Puede hacer un análisis básico, comenzando por letras individuales, pero puede migrar a un análisis de varias letras más complejo; por ejemplo, U casi siempre sigue a Q, y solo ciertas letras pueden aparecer dos veces seguidas.

Ejemplos

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

Desafíos

Vea si puede descifrar el texto en cada uno de estos cifrados:

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

Tengo las matrices de sustitución y el texto en claro para cada una, pero solo las revelaré si se vuelve demasiado difícil o si alguien no lo resuelve.

La solución que puede descifrar la mayoría de los mensajes con éxito es el ganador. Si dos soluciones son igualmente buenas, se decidirán por recuento de votos.

Thomas O
fuente
3
¿Qué define »más elegante«? Creo que eso es lo mismo que Chris objetó en 99 botellas. Es un criterio subjetivo que es bastante difícil de juzgar.
Joey
@Joey ¿La mayoría de los votos a favor? Deje que la comunidad decida.
Thomas O
2
Re "mayoría de votos": no estoy contento de ver que esto se convierta en una publicación de concurso de popularidad, sobre todo porque la publicación es excelente; ver meta.codegolf.stackexchange.com/questions/110/… para mis pensamientos sobre todo el asunto.
Chris Jester-Young
2
¿Qué significa "elegante" aquí? ¿El mejor rendimiento de big-O?
gnibbler
1
@ Bass5098, no. Es solo un texto cifrado difícil que se ha contaminado para hacerlo más resistente al análisis de frecuencia.
Thomas O

Respuestas:

9

Pitón

He descubierto todas las frases secretas, pero no las publicaré aquí. Ejecute el código si le importa.

El código funciona seleccionando un carácter de espacio, enumerando todas las sustituciones posibles para cada palabra, luego buscando sustituciones compatibles. También permite que algunas palabras fuera del léxico se ocupen de errores ortográficos en texto claro :)

Usé un léxico grande (~ 500K palabras) 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
fuente
1

PHP (incompleto)

Esta es una solución PHP incompleta que funciona utilizando la información de frecuencia de letras en la pregunta más un diccionario de palabras emparejadas con expresiones regulares basadas en las letras más confiables en la palabra dada.

En la actualidad, el diccionario es bastante pequeño, pero con la expansión adecuada, anticipo que los resultados mejorarían. He considerado la posibilidad de coincidencias parciales, pero con el diccionario actual esto da como resultado una degradación en lugar de una mejora en los resultados.

Incluso con el pequeño diccionario actual, creo que puedo decir con bastante seguridad qué codifica el cuarto mensaje.

#!/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
fuente
Intente usar / usr / share / dict / words si está en un sistema que lo tiene.
Keith Randall el