C# :: Aufgabe #120

2 Lösungen Lösungen öffentlich

Substitutionschiffren knacken

Fortgeschrittener - C# von eulerscheZhl - 09.03.2016 um 21:41 Uhr
Bei einer Substitutionschiffre werden Buchstaben durch andere Buchstaben ersetzt.
Dadurch bleiben die Häufigkeitsverteilungen der Buchstaben allerdings erhalten, weshalb etwa ein 'e' leicht erkannt werden kann.

Für diese Aufgabe soll aber ein Wörterbuch (z.B. von der Uni Kiel, benötigt aber etwas Nachbearbeitung) verwendet werden, um die ursprüngliche Nachricht zu erhalten.
So ist es zwar schwer, eine komplett richtige Dekodierung zu erhalten (da nicht alle Wörter im Wörterbuch enthalten sind), aber man kann lesbare Ergebnisse erzielen.

Im Anhang befinden sich Texte aus zufälligen Artikeln der deutschsprachigen Wikipedia. Es ist jeweils mindestens die Hälfte der vorkommenden Wörter im verlinkten Wörterbuch enthalten.

Lösungen:

vote_ok
von daniel59 (4260 Punkte) - 17.05.2016 um 07:41 Uhr
Ich habe für die Suche der Wörter zunächst die Klasse StringPattern.cs (s.u.) erstellt und für jedes Wort im Wörterbuch ein Muster erstellt. Danach mache ich das mit jedem Wort im Text nochmal. Dabei achte ich darauf, dass auch nur die 30 Zeichen im Wort enthalten sind. Im ersten Musterabgleich suche ich nur die Wörter raus die das gleiche Muster haben (Equals), im zweiten suche ich dann die Wörter die bereits die eindeutigen Zeichen enthalten raus.
Mit der ExceptDictionary-Methode entferne ich aus dem möglichen Alphabeten die Zeichen, die eindeutig sind z.B. das "M" kann nur zu "E" werden. Wenn jetzt beim "N" die Zeichen "W" und "E" möglich wird das "E" entfernt. Dies wird nach beiden Abgleichen durchgeführt.
Bei den Abgleichen werden dann Zeichen für Zeichen kleine Geheimalphabete erstellt und den Möglichkeiten hinzugefügt. Dabei wird alte Möglichkeit durch die Gemeinsamkeiten des alten und des neuen Alphabets ersetzt.
Jetzt ist bereits der Großteil der Zeichen eindeutig, also ein Zeichen "M" kann nur ein Zeichen "E" in Wirklichkeit sein. Für die mehrdeutigen Zeichen werden jetzt alle Möglichkeiten der Anordnung im Geheimalphabet überprüft und so mögliche dechriffierte Texte erstellt. Sollte das nicht möglich sein wird ein Text erstellt, wobei nur die eindeutigen Zeichen ersetzt werden.

In der angehängten .zip-Datei sind alle dechriffrierten Texte enthalten.

Quellcode ausblenden C#-Code
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;

namespace ConsoleSubChiffre
{
    //Es wurde folgendes Wörterbuch genutzt:
    //https://sourceforge.net/projects/germandict/?source=typ_redirect + "daß"
    class Program
    {
        static readonly char[] chars = { ' ', ',', '.', ':', ';', '!', '?', '\t', '\n', '\r', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '\"', '\'', '(', ')', '[', ']', '{', '}', '+', '-', '*', '/', '°', '^', '§', '§', '%', '=', '\\', '_', '>', '<', '|', '#', '~', '€', '@', '┼' };
        static readonly string AllChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜß";
        static Dictionary<char, char> pAlphabet;
        static Dictionary<char, List<char>> possibilties = new Dictionary<char, List<char>>();
        static void Main(string[] args)
        {
            pAlphabet = new Dictionary<char, char>();
            string[] allWords = (from word in Properties.Resources.germanDictionary.ToUpper().Split('\n', '\r') where !string.IsNullOrWhiteSpace(word) && Array.TrueForAll(word.ToCharArray(), c => AllChars.Contains(c)) select word).Distinct().ToArray();
            var patterns = StringPattern.CreatePatterns(allWords).ToList();//siehe Klasse StringPattern


            StreamReader sr = new StreamReader(@"C:\Users\Daniel\Downloads\240\chiffre01.txt");
            string text = sr.ReadToEnd().ToUpper();
            sr.Close();
            Console.WriteLine(text + "\n\n");

            Dictionary<char, List<char>> alphabet = new Dictionary<char, List<char>>();
            string[] split = (from word in text.Split(chars) where !string.IsNullOrWhiteSpace(word) && Array.TrueForAll(word.ToCharArray(), c => AllChars.Contains(c)) select word).ToArray();
            int longestWord = split.Max(s => s.Length);

            List<StringPattern> textPattern = new List<StringPattern>();
            foreach (string s in split.Distinct())//Alle Wörter die das gleiche Muster haben finden
            {
                StringPattern pattern = new StringPattern(s);
                textPattern.Add(pattern);
                var possible = (from p in patterns where p.Equals(pattern) select p.String).ToList();
                if (possible.Count == 0)
                { continue; }
                alphabet = new Dictionary<char, List<char>>();
                for (int i = 0; i < s.Length; i++)
                {
                    if (alphabet.ContainsKey(s[i]))
                    { continue; }
                    alphabet.Add(s[i], new List<char>());
                    foreach (var item in possible)
                    {
                        alphabet[s[i]].Add(item[i]);
                    }
                }
                AddAlphabet(alphabet);
            }
            ExceptDictionary();


            foreach (StringPattern pat in textPattern)//Alle Wörter die das gleiche Muster haben und die eindeutigen Buchstaben enthalten finden
            {
                string temp = "";
                List<int> ignoreIndexes = new List<int>();
                for (int i = 0; i < pat.String.Length; i++)
                {
                    if (possibilties.ContainsKey(pat.String[i]))
                    {
                        if (possibilties[pat.String[i]].Count == 1)
                        {
                            temp += possibilties[pat.String[i]][0];
                            ignoreIndexes.Add(i);
                        }
                        else
                        {
                            temp += pat.String[i];
                        }
                    }
                    else
                    {
                        temp += pat.String[i];
                    }
                }

                if (ignoreIndexes.Count == temp.Length)
                { continue; }
                StringPattern pattern = new StringPattern(temp);
                var possible = (from p in patterns where p.IgnoreEquals(pattern, ignoreIndexes) select p.String).ToList();
                if (possible.Count == 0)
                { continue; }
                alphabet = new Dictionary<char, List<char>>();
                for (int i = 0; i < temp.Length; i++)
                {
                    if (possibilties[pat.String[i]].Count == 1)
                    { continue; }
                    if (alphabet.ContainsKey(pat.String[i]))
                    { continue; }
                    alphabet.Add(pat.String[i], new List<char>());
                    foreach (var item in possible)
                    {
                        alphabet[pat.String[i]].Add(item[i]);
                    }
                }
                AddAlphabet(alphabet);
            }
            ExceptDictionary();

            string result = "";
            List<KeyValuePair<string, int>> results = new List<KeyValuePair<string, int>>();
            new ForEachSelfLoop<List<char>, char>(possibilties.Values, delegate (char[] array)//siehe Klasse ForEachSelfLoop//alle möglichen Buchstabenkombinationen testen
            {
                if (array.Distinct().Count() != possibilties.Count)
                { return LoopKey.Continue; }
                pAlphabet = new Dictionary<char, char>();
                result = "";
                for (int i = 0; i < array.Length; i++)
                {
                    char key = possibilties.Keys.ElementAt(i);
                    char value = array[i];
                    pAlphabet.Add(key, value);
                }

                foreach (char c in text)
                {
                    if (pAlphabet.ContainsKey(c))
                    { result += pAlphabet[c]; }
                    else
                    { result += c; }
                }

                int count = (from word in result.Split(chars) where !string.IsNullOrWhiteSpace(word) select word).ToArray().Count(x => allWords.Contains(x)); ;
                if (count > (70 * split.Length) / 100)//70% muss richtig sein
                {
                    results.Add(new KeyValuePair<string, int>(result, count));
                }

                return LoopKey.Continue;
            });

            if (results.Count == 0)
            {
                result = "";
                foreach (char c in text)
                {
                    if (possibilties.ContainsKey(c))
                    {
                        if (possibilties[c].Count == 1)
                        {
                            result += possibilties[c][0];
                        }
                        else
                        {
                            result += c;
                        }
                    }
                    else
                    {
                        result += c;
                    }
                }
                Console.WriteLine("\n\n" + result);
            }
            else
            {
                int max = results.Max(x => x.Value);

                var maxResults = from res in results where res.Value == max select res.Key;
                foreach (var res in maxResults)
                {
                    Console.WriteLine("\n\n" + res);
                }
            }
            Console.ReadLine();
        }

        static IEnumerable<T> GetCommonalities<T>(IEnumerable<T> source1, IEnumerable<T> source2)
        {
            foreach (T item in source1)
            {
                if (source2.Contains(item))
                { yield return item; }
            }
        }

        static int oldCount = -1;
        static void ExceptDictionary()
        {
            var one = from p in possibilties
                      where p.Value.Count == 1
                      select new KeyValuePair<char, char>(p.Key, p.Value[0]);

            if (one.Count() == oldCount)
            { return; }

            oldCount = one.Count();
            foreach (var item in one)
            {
                var containsOne = from p in possibilties
                                  where p.Value.Count > 1 && p.Value.Contains(item.Value)
                                  select p;

                foreach (var key in containsOne)
                {
                    possibilties[key.Key].Remove(item.Value);
                }
            }
            ExceptDictionary();
        }

        static void AddAlphabet(Dictionary<char, List<char>> item)
        {
            if (possibilties.Count == 0)
            {
                possibilties = item;
            }
            else
            {
                foreach (var el in item)
                {
                    if (!possibilties.ContainsKey(el.Key))
                    {
                        possibilties.Add(el.Key, new List<char>(el.Value));
                    }
                }

                for (int i = 0; i < possibilties.Count; i++)
                {
                    char key = possibilties.Keys.ElementAt(i);
                    if (item.ContainsKey(key))
                    {
                        List<char> temp = GetCommonalities(possibilties[key], item[key]).Distinct().ToList();
                        if (temp.Count == 0)
                        { continue; }

                        possibilties[key] = temp;
                    }
                }
            }
        }
    }
}


Die Klasse StringPattern.cs
Beispiel: Die Wörter GEHEN, SEHEN und GEBEN haben das gleiche Muster: 1. Zeichen an Index 0, 2. Zeichen an den Indizes 1 & 3, 3.Zeichen an Index 2, 4. Zeichen an Index 4. Hat ein chiffriertes Wort dieses Muster so kann es erstmal jedes der 3 Worte sein (Equals-Methode). Wenn man das Wort schon z.T. dechriffiert hat z.B. "ZEBEN" kann man mittels der IgnoreEquals-Methode die Indizes 1,2,3,4 ignorieren, aber das Zeichen muss übereinstimmen. Jetzt kann hier nur noch GEBEN von den 3 Wörten stimmen.

Quellcode ausblenden C#-Code
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleSubChiffre
{
    class StringPattern
    {
        private string _string;
        public string String { get { return _string; } set { _string = value; CreateIndexes(); } }

        private Dictionary<char, List<int>> indexes;
        public Dictionary<char, List<int>> Indexes { get { return indexes; } }

        public StringPattern(string String)
        {
            indexes = new Dictionary<char, List<int>>();
            this.String = String;
        }

        private void CreateIndexes()
        {
            indexes.Clear();
            foreach (char c in this.String.Distinct())
            {
                indexes.Add(c, FindAllIndexes(this.String, x => x == c).ToList());
            }
        }

        static IEnumerable<int> FindAllIndexes<T>(IEnumerable<T> source, Predicate<T> predicate)
        {
            int i = 0;
            foreach (var item in source)
            {
                if (predicate(item))
                { yield return i++; }
                else
                { i++; }
            }
        }

        public bool IgnoreEquals(StringPattern obj, IEnumerable<int> ignoreIndexes)//Ignoriere beim Vergleich die Indizes, aber Schlüssel muss gleich sein
        {
            StringPattern comparison = obj;
            if (comparison.Indexes.Count != this.Indexes.Count || this.String.Length != comparison.String.Length)
            {
                return false;
            }
            for (int i = 0; i < this.Indexes.Count; i++)
            {

                KeyValuePair<char, List<int>> thisPair = this.Indexes.ElementAt(i);
                KeyValuePair<char, List<int>> compPair = comparison.Indexes.ElementAt(i);

                if (ignoreIndexes.Contains(i))
                {
                    if (thisPair.Key != compPair.Key)
                    { return false; }
                    continue;
                }

                if (thisPair.Value.Count != compPair.Value.Count)
                {
                    return false;
                }
                for (int j = 0; j < thisPair.Value.Count; j++)
                {
                    if (thisPair.Value[j] != compPair.Value[j])
                    {
                        return false;
                    }
                }
            }
            return true;
        }

        public override bool Equals(object obj)
        {
            StringPattern comparison = (StringPattern)obj;
            if (comparison.Indexes.Count != this.Indexes.Count || this.String.Length != comparison.String.Length)
            {
                return false;
            }
            for (int i = 0; i < this.Indexes.Count; i++)
            {
                KeyValuePair<char, List<int>> thisPair = this.Indexes.ElementAt(i);
                KeyValuePair<char, List<int>> compPair = comparison.Indexes.ElementAt(i);
                if (thisPair.Value.Count != compPair.Value.Count)
                {
                    return false;
                }
                for (int j = 0; j < thisPair.Value.Count; j++)
                {
                    if (thisPair.Value[j] != compPair.Value[j])
                    {
                        return false;
                    }
                }
            }
            return true;
        }

        public static IEnumerable<StringPattern> CreatePatterns(IEnumerable<string> source)
        {
            foreach (string s in source)
            {
                yield return new StringPattern(s);
            }
        }
    }
}


Die Klasse ForEachSelfLoop.cs
Quellcode ausblenden C#-Code
using System.Collections.Generic;
using System.Linq;

namespace ConsoleSubChiffre
{
    enum LoopKey { Break, Continue }
    delegate T2 PFunc<in T1, out T2>(params T1[] parameter);

    sealed class ForEachSelfLoop<T1, T2> where T1 : IEnumerable<T2>
    {
        private T2[] Parameter;
        private int currentDepth = 1;

        public PFunc<T2, LoopKey> Action { get; set; }
        private int count;
        public int Count { get { return count; } }
        public IEnumerable<T1> Source { get; set; }
        private bool breakUp = false;

        public ForEachSelfLoop(IEnumerable<T1> source, PFunc<T2, LoopKey> action)
        {
            Source = source;
            count = Source.Count();
            breakUp = false;
            Action = action;
            Parameter = new T2[count];
            Invoke();
        }

        private void Invoke()
        {
            if (breakUp)
            { return; }
            foreach (T2 item in Source.ElementAt(currentDepth - 1))
            {
                Parameter[currentDepth - 1] = item;
                if (currentDepth == Count)
                {
                    LoopKey key = Action.Invoke(Parameter);
                    if (key == LoopKey.Break)
                    {
                        breakUp = true;
                        return;
                    }
                }
                else
                {
                    currentDepth++;
                    Invoke();
                }
            }
            currentDepth--;
        }
    }

}
vote_ok
von eulerscheZhl (5230 Punkte) - 04.06.2016 um 16:41 Uhr
Quellcode ausblenden C#-Code
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
//Flags zum Programmaufruf
using Mono.Options; //https://github.com/mono/mono/blob/master/mcs/class/Mono.Options/Mono.Options/Options.cs

namespace Substitionschiffre
{
	class Chiffre
	{
		static Dictionary<string, string[]> cipher;
		static int bestFit;
		static string input;

		static void Main(string[] args) {
			string dictFile = null;
			string chiffreFile = null;
			string outputFile = null;
			bool scramble = false;
			OptionSet p = new OptionSet () {
				{ "d|dict=", "the dictionary filename.",
					v => dictFile = v },
				{ "c|chiffre=", "the chiffre filename.",
					v => chiffreFile = v },
				{ "o|output=", "the output filename.",
					v => outputFile = v },
				{ "s|scrable", "scramble a text, instead of decoding",
					v => { if (v != null) scramble = true; } },
			};
			p.Parse (args);

			if (dictFile == null || chiffreFile == null) {
				Console.WriteLine ("usage: dict=<dictFile> chiffre=<chiffreFile>");
				return;
			}

			//einlesen
			string[] dict = File.ReadAllLines (dictFile).Where (line => !line.StartsWith ("#")).
				Select (x => x.Split (" \t".ToCharArray (), StringSplitOptions.RemoveEmptyEntries) [0].ToLower()).
					Distinct ().ToArray ();

			input = File.ReadAllText (chiffreFile);
			string[] words = input.ToLower ().Split (" .:!?,();-'\"1234567890\n".ToCharArray (), StringSplitOptions.RemoveEmptyEntries).Distinct ().ToArray ();
			FindCandidates (words, dict);
			if (scramble && outputFile != null) {
				Scramble.ScrambleText (chiffreFile, outputFile);
				Console.WriteLine ("known words: " + words.Count(x => dict.Contains(x)) + "/" + words.Length);
				return;
			}
			bestFit = cipher.Count * 1 / 2; //mindestens die Hälfte der Wörter muss bekannt sein
			Console.WriteLine ("starting with " + bestFit + " words");
			//buchstabenhäufigkeit
			Dictionary<char, int> letterDict = new Dictionary<char, int> ();

			string alphabet = new string (input.ToCharArray ().Distinct ().ToArray());

			foreach (string word in words) {
				foreach (char c in word) {
					if (!alphabet.Contains (c))
						continue;
					if (!letterDict.ContainsKey (c))
						letterDict.Add (c, 0);
					letterDict [c]++;
				}
			}
			List<KeyValuePair<char, int>> letterCount = letterDict.OrderBy (x => -x.Value).Where (x => x.Value > 0).ToList ();
			char[] nextLetter = (new string (letterCount.Select (x => x.Key).ToArray ()) + "abcdefghijklmnopqrstuvwxyzäöüß").Distinct ().ToArray ();

			Dictionary<char, char> letters = new Dictionary<char, char> ();
			Dictionary<char,bool> used = new Dictionary<char, bool> ();
			foreach (char c in nextLetter) {
				used.Add (c, false);
			}
			FindLetters (letters, used, nextLetter, 0);
		}

		static void FindCandidates(string[] words, string[] dict) {
			cipher = new Dictionary<string, string[]> ();
			foreach (string word in words) {
				List<string> matches = new List<string> ();
				foreach (string cand in dict) {
					if (cand.Length != word.Length)
						continue;
					bool canAdd = true;
					Dictionary<char, char> subst = new Dictionary<char, char> ();
					for (int i = 0; i < word.Length; i++) {
						if (!subst.ContainsKey (word [i])) {
							if (subst.ContainsValue (cand [i])) {
								canAdd = false;
								break;
							}
							subst.Add (word [i], cand [i]);
						}
						if (subst [word [i]] != cand [i]) {
							canAdd = false;
							break;
						}
					}
					if (canAdd)
						matches.Add (cand);
				}
				cipher.Add (word, matches.ToArray());
			}
		}

		static void FindLetters(Dictionary<char, char> letters, Dictionary<char, bool> used, char[] nextLetter, int index) {
			int fit = CountFit (letters);
			if (fit < bestFit)
				return;
			if (index == nextLetter.Length) { //Alle Buchstaben zugeteilt, Bilanz ziehen und gegebenenfalls Ergebnis ausgeben
				bestFit = fit;
				foreach (string word in cipher.Keys) {
					StringBuilder sb = new StringBuilder ();
					foreach (char c in word) {
						sb.Append (letters.ContainsKey (c) ? letters [c] : '?');
					}
					Console.Write (sb.ToString().PadRight(10));
				}
				Console.Write ("\n" + fit + " bekannte Worte\n" + String.Join (", ", letters.Select (x => x.Key + "=" + x.Value)));
				char[] message = input.ToCharArray();
				for (int i = 0; i < message.Length; i++) {
					if (letters.ContainsKey (message [i]))
						message [i] = letters [message [i]];
				}
				Console.WriteLine ("\ndecoded message: " + new string (message) + "\n\n");
				return;
			}

			List<KeyValuePair<char, int>> fitCount = new List<KeyValuePair<char, int>> ();
			foreach (char c in used.Where(x => !x.Value).Select(x=>x.Key)) {
				Dictionary<char, char> newDict = new Dictionary<char, char> (letters);
				newDict.Add (nextLetter [index], c);
				fitCount.Add (new KeyValuePair<char, int>(c, CountFit (newDict)));
			}
			fitCount = fitCount.OrderBy (x => -x.Value).ToList();
			foreach(KeyValuePair<char, int> pair in fitCount) {
				if (pair.Value < bestFit)
					return;
				Dictionary<char, char> newDict = new Dictionary<char, char> (letters);
				newDict.Add (nextLetter [index], pair.Key);
				Dictionary<char, bool> newUsed = new Dictionary<char, bool> (used);
				newUsed [pair.Key] = true;
				FindLetters (newDict, newUsed, nextLetter, index + 1);
			}
		}

		private static int CountFit(Dictionary<char, char> letters) {
			int result = 0;
			foreach (KeyValuePair<string, string[]> pair in cipher) {
				bool match = false;
				foreach (string cand in pair.Value) {
					bool currentMatch = true;
					for (int i = 0; i < pair.Key.Length; i++) {
						if (letters.ContainsKey (pair.Key [i]) && letters [pair.Key [i]] != cand [i]) {
							currentMatch = false;
							break;
						}
					}
					if (currentMatch) {
						match = true;
						break;
					}
				}
				if (match)
					result++;
			}
			return result;
		}
	}
}


Und zum Zerwürfeln:
Quellcode ausblenden C#-Code
using System;
using System.Collections.Generic;
using System.IO;

namespace Substitionschiffre
{
	public static class Scramble
	{
		public static void ScrambleText(string inputFile, string outputFile) {
			string alphabet = "abcdefghijklnmopqrstuvwxyzöäüß";
			char[] scramble = alphabet.ToCharArray ();
			Random r = new Random ();
			for (int i = 0; i < scramble.Length; i++) {
				int index = r.Next (i, scramble.Length);
				char tmp = scramble [index];
				scramble [index] = scramble [i];
				scramble [i] = tmp;
			}
			Dictionary<char,char> dict = new Dictionary<char, char> ();
			for (int i = 0; i < alphabet.Length; i++) {
				dict.Add (alphabet [i], scramble [i]);
			}
			char[] text = File.ReadAllText (inputFile).ToLower().ToCharArray ();
			for (int i = 0; i < text.Length; i++) {
				if (dict.ContainsKey (text [i]))
					text [i] = dict [text [i]];
			}
			File.WriteAllText (outputFile, new string(text));
		}
	}
}