Индекс совпадений ФридманаИндекс совпадений — один из методов криптоанализа шифра Виженэра. Описание было опубликовано Уильямом Фридманом в 1920. Метод основывается на вычислении вероятности того, что два случайных элемента текста совпадут. Эту вероятность называют индексом совпадений. Уильям Фридман показал, что значения индекса совпадений существенно отличаются для текстов различной природы. Это позволяет сначала определить длину ключа шифра, а затем найти и сам ключ. Появление метода индекса совпадений открыло новые возможности в криптоанализе шифра Виженэра. По сравнению с методом Касиски, метод индекса совпадений менее трудоемок, требует меньшей длины текста, более пригоден для автоматизации и менее подвержен ошибкам. Индекс совпадений более эффективен и допускает анализ шифров с длинными ключами.Формула для вычисления индекса совпаденийДопустим у нас есть текст, написанный на неизвестном языке. Алфавит данного языка состоит из [math]m[/math] символов. В нашем распоряжении есть строка [math]\vec {x}[/math] из [math]n[/math] символов. Индекс совпадения это вероятность совпадения двух произвольных символов в строке. Если [math]f_i[/math] — это количество [math]i[/math]-го символа алфавита в строке [math]\vec {x}[/math], тогда индекс совпадения вычисляется по формуле:[math] I({\vec {x}})=\sum \limits _{i=1}^{m}\displaystyle{\frac {f_i(f_{i}-1)}{n(n-1)}}[/math] Открытый текстДопустим, строка [math]\vec {x}[/math] открытый текст или результат любого моноалфавитного шифрования. Индекс совпадений выражают через [math]p_i[/math] — вероятности появления [math]i[/math]-го символа. Получаем формулу: [math]I({\vec {x})=\sum \limits _{i=1}^{m}p_{i}^{2}} [/math] Величины [math]p_i[/math] имеют определенные значения, для открытого текста индекс совпадений не зависит от его содержания, а зависит от языка, на котором написан текст. Индекс совпадения также зависит от типа текста (художественная литература, математические или химические тексты), от времени написания (в текстах на русском языке написанных до 1918 будет обилие ъ и ѣ), а также зависит от автора текста. Значения [math]p_i[/math] исследованы и известны, это позволяет рассчитать значения индекса совпадений открытого текста для различных языков. языкиндекс совпаденийисточникрусский0.0553Пилиди В. С. Криптография. Вводные главыанглийский0.0644William Frederick Friedman. Military Cryptanalysis. Part II. Simpler vrieties of polyalphabetic substitution systems (p. 117)немецкий0.0762французский0.0778испанский0.0775португальский0.0746итальянский0.0738 Код (Python): SYMBOLSEN = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' SYMBOLSRU = 'АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ' import re def friedman(text,SYMBOLS): n = 0 freq = [0 for i in range(len(SYMBOLS))] for c in text: freq[SYMBOLS.find(c.upper())] += 1 n += 1 ic = 0 for f in freq: ic += f * (f - 1) ic = float(ic) / float((n * (n - 1))) return ic def main(): autorEn = ['Льюис Кэрролл','Эрл Стенли Гарднер','Джоан Роулинг'] autorRu = ['А.С.Пушкин','Л.Н.Толстой','А.А.Бушков'] plaintext_icEn = [] plaintext_icRu = [] yearEn = ['1864','1934','1997'] yearRu = ['1833','1863','1996'] filenameEn = ['Carroll.txt','Gardner.txt','Rowling.txt'] filenameRu = ['Pushkin.txt','Tolstoy.txt','Bushkov.txt'] print('\u2500' * 19+'\u252C'+'\u2500'*26+'\u252C'+'\u2500'*6) print(' '*7+'Автор'+' '*7+'\u2502Индекс совпадений Фридмана\u2502 год') print('\u2500' * 19+'\u2534'+'\u2500' * 26+'\u2534'+'\u2500' * 6) print(' ' * 22+'для английского языка') print('\u2500' * 19+'\u252C'+'\u2500'*26+'\u252C'+'\u2500'*6) for i in range(len(autorEn)): with open(filenameEn[i], 'r', encoding='utf-8') as file: plaintext = file.read() plaintext = re.sub('[^A-Z]','',plaintext.upper()) plaintext_icEn.append(friedman(plaintext,SYMBOLSEN)) print(autorEn[i]+' '*(19-len(autorEn[i]))+'\u2502 %.019f'%plaintext_icEn[i],' \u2502',yearEn[i]) print('\u2500' * 19+'\u2534'+'\u2500' * 26+'\u2534'+'\u2500' * 6) print(' ' * 22+'для русского языка') print('\u2500' * 19+'\u252C'+'\u2500'*26+'\u252C'+'\u2500'*6) for i in range(len(autorRu)): with open(filenameRu[i], 'r', encoding='utf-8') as file: plaintext = file.read() plaintext = re.sub('[^А-Я]','',plaintext.upper()) plaintext_icRu.append(friedman(plaintext,SYMBOLSRU)) for i in range(len(autorRu)): print(autorRu[i]+' '*(19-len(autorRu[i]))+'\u2502 %.019f'%plaintext_icRu[i],' \u2502',yearRu[i]) print('\u2500' * 19+'\u2534'+'\u2500' * 26+'\u2534'+'\u2500' * 6) print('\nИндексы совпадений для других языков') print('\u2500' * 14+'\u252C'+'\u2500' * 7) langs = ['английский','русский','итальянский','испанский','немецкий','французский','португальский'] IC = [0.0662,0.0553,0.0738,0.0775,0.0762,0.0778,0.0746] for i in range(len(langs)): print(langs[i]+' '*(14-len(langs[i]))+'\u2502'+str(IC[i])) print('\u2500' * 14+'\u2534'+'\u2500' * 7) if __name__ == '__main__': main() Использовались тексты: Бушков "Охота на Пиранью", Пушкин "Капитанская дочка", Толстой "Война и мир", Кэрролл "Алиса в стране чудес", Гарднер "Дело о воющей собаке", Роулинг "Гари Потер и философский камень"
Взлом шифра простой замены с использованием словарного шаблона (Продолжение)Функция hackSimpleSub() возвращает пересечение словарей шифробукв, из которого удалены дешифрованные буквы. Затем это пересечение передается функции decryptWithCipherletterMapping() для дешифрования шифротекста, сохраненного в переменной message. Пересечение, сохраненное в переменной letterMapping, - это словарь, ключами которого являются 26 однобуквенных строк в верхнем регистре, представляющих шифробуквы. Значениями словаря будут списки вариантов дешифрования для каждой шифробуквы. Если каждой шифробукве сопоставлен только один вариант дешифрования, тогда имеем полностью решенный словарь и можем расшифровать любой шифротекст, в котором используются те же шифр и ключ. Каждый сгенерированный словарь шифробукв зависит от используемого шифротекста. Иногда мы получим лишь частично решенный словарь, в котором для некоторых шифробукв отсутствуют варианты дешифрования или остается несколько вариантов. Короткие шифротексты, содержащие не все буквы алфавита, будут с большей вероятностью приводить к неполным словарям. В программе вызывается функция print() для вывода на экран переменной letterMapping, исходного шифротекста и дешифрованного сообщения. Дешифрованное сообщение сохраняется в переменной hackedМessage, которая выводится на экран, чтобы пользователь мог проверить текст. Для получения дешифрованного сообщения использется функцию decryptWithCipherletterMapping().Создание словарей шифро6уквПрограмма строит дешифровальный словарь для каждого слова шифротекста. Для этого потребуется несколько вспомогательных функций. Одна из них создает пустой словарь шифробукв, и ее вызывают для каждого шифрослова. Еще одна функция получает шифрослово, текущий словарь шифробукв и слово-кандидат. Эту функцию вызывают для каждого шифрослова и каждого слова-кандидата. Она добавляет все варианты дешифрования, определенные на основе данного слова-кандидата, в дешифровальный словарь шифрослова и возвращать этот словарь. Получив словари шифробукв для нескольких слов шифротекста, используем функцию для их объединения. И еще одна вспомогательная функция будет находить максимально возможное число решений, сопоставляя дешифрованные буквы с каждой шифробуквой. Создание пустого словаря шифро6уквСначала необходимо создать пустой словарь шифробукв. Код (Python): def getBlankCipherletterMapping(): # возвращает словарь, ключами которого являются односимвольные строки, # соответствующие 26 буквам алфавита. return {'A':[],'B':[],'C':[],'D':[],'E':[],'F':[],'G':[],'H':[],'I':[], 'J':[],'K':[],'L':[],'M':[],'N':[],'O':[],'P':[],'Q':[],'R':[], 'S':[],'T':[],'U':[],'V':[],'W':[],'X':[],'Y':[],'Z':[]} Функция getBlankCipherletterMapping() возвращает словарь, ключами которого являются односимвольные строки, соответствующие 26 буквам алфавита.Добавление букв в дешифровальный словарьСоздается функция addLettersToMapping(), предназначенная для добавления букв в словарь. Код (Python): def addLettersToMapping(letterMapping, cipherword, candidate): У функции три параметра: словарь шифробукв (letterMapping), шифрослово (cipherword) и слово - кандидат, в которое может быть дешифровано данное шифрослово (candidate). Функция сопоставляет каждую букву слова-кандидата с шифробуквой, занимающей соответствующую позицию в шифрослове, и добавляет эту букву в словарь letterMapping, если ее там еще нет. Если 'PUPPY' - слово-кандидат для шифрослова 'HGHHU', тогда функция addLettersToMapping() добавит в словарь letterMapping значение 'Р' для ключа 'Н'. Затем функция перейдет к следующей букве и присоединит 'U' к списку, связанному с ключом 'G'. Если буква уже находится в списке вариантов дешифрования, то функция addLettersToMapping() не добавит ее повторно в список. В слове 'PUPPY' функция не станет добавлять букву 'Р' в ключ 'Н' для следующих двух вхождений буквы 'Р', так как она там уже имеется. Функция изменит значение ключа 'U', добавив в его список вариантов дешифрования букву 'Y'. В функции addLettersToMapping() предполагается, что вызовы len(cipherword) и len(candidate) возвращают одно и то же значение, поскольку передаются только пары "шифрослово - кандидат" с совпадающими шаблонами. Функция проходит по каждому индексу строки cipherword, чтобы проверить, не добавлена ли соответствующая буква в список вариантов дешифрования. Код (Python): for j in range(len(cipherword)): if candidate[j] not in letterMapping [cipherword[j]]: letterMapping[cipherword[j]].append(candidate[j]) В процессе итерирования по буквам шифрослова в качестве индекса в слове-кандидате тоже используется переменная j. Мы можем так поступать, поскольку вариантом дешифрования для шифробуквы cipherword[j], подлежащим добавлению, является буква candidate[j]. Например, для доступа к первым буквам каждой из строк будут использоваться выражения cipherword[0] и candidate[0]. Далее выполнение переходит к инструкции if. Эта инструкция проверяет отсутствие варианта candidate[j] в списке вариантов для данной шифробуквы, пропуская варианты, уже имеющиеся в списке. Соответствующая буква в словаре ищется с помощью выражения letterMapping[cipherword[j]], поскольку cipherword[j] - это ключ в словаре letterMapping, к которому необходимо осуществить доступ. Такая проверка нужна для того, чтобы предотвратить дублирование букв в списке вариантов дешифрования. Первая буква 'Р' в слове 'PUPPY' может быть добавлена в словарь letterMapping на первой итерации цикла, но когда j станет равно 2 на третьей итерации, буква 'Р' в позиции candidte[2] не будет добавлена в словарь, поскольку она уже была добавлена в него на первой итерации. Если вариант дешифрования отсутствует в словаре, то новая буква, candidate[j], добавляется в список вариантов дешифрования по ключу letterMapping[cipherword[j]]. В Python в качестве параметра передается ссылка на словарь, а не сам словарь, поэтому любые изменения, внесенные в словарь letterMapping в функции addLettersToMapping(), будут сохранены и после ее завершения. Соответственно, изменится также словарь candidateMap, передаваемый в функцию addLettersToMapping(). Пройдя по всем индексам в строке cipherword, функция завершит добавление букв в словарь letterMapping. Теперь необходимо выяснить, как программа сравнивает полученный словарь со словарями других шифрослов для выявления пересечений. Пересечение двух словарейФункция hackSimpleSub() использует функцию intersectMappings(), которая получает два словаря шифробукв, передаваемых ей в качестве параметров mapA и mapB, и возвращает объединенный словарь. Предварительно создается пустой словарь, а затем в него добавляются варианты дешифрования букв, но только в том случае, если они имеются в обоих словарях, что позволяет избежать появления дубликатов. Код (Python): def intersectMappings(mapA, mapB): # Чтобы построить пересечение словарей, создаем пустой словарь и # добавляем в него только варианты, существующие в ОБОИХ словарях intersectedMapping = getBlankCipherletterMapping() С помощью функции getBlankCipherletterMapping() создается пустой словарь шифробукв, который сохраняется в переменной intersectedMapping. В цикле for перебираются все буквы, хранящиеся в константе LETTERS. Переменная letter используется в качестве ключа в словарях mapA и mapB. Код (Python): for letter in LETTERS: # Пустой список означает "возможна любая буква"; # в этом случае копируем список целиком if mapA[letter] == []: intersectedMapping[letter] elif mapB[letter] == []: intersectedМapping[letter] copy.deepcopy (mapB[letter]) copy.deepcopy(mapA[letter]) Проверяется, не является ли список вариантов дешифрования буквы в словаре mapA пустым. Пустой список означает, что данная шифробуква может быть дешифрована в любую букву. В таком случае в объединенный словарь копируется весь список вариантов дешифрования из словаря mapB. Аналогичная проверка делается для словаря mapB. Если оба списка пустые, то условие вернет Тrue, и тогда в объединенный словарь будет скопирован пустой список из mapB. Блок else обрабатывает случай, когда ни список mapA, ни список mapB для данной буквы не являются пустыми. Код (Python): else: # Если буква из списка mapA[letter] существует в списке # mapB[letter], добавляем ее в intersectedМapping[letter] for mappedLetter in mapA[letter]: if mappe dLetter in mapB[letter]: intersectedMapping[letter].append(mappedLetter) return intersectedMapping Если списки не пустые, то организуется цикл по буквам, хранящимся в списке mapA[letter]. Проверяется, встречается ли буква в списке mapB[letter]. Если это так, то совпадающая буква добавляется в список intersectedMapping[letter] объединенного словаря. По окончании цикла словарь intersectedMapping должен содержать лишь варианты дешифрования букв, которые существуют как в словаре mapA, так и в словаре mapB. Этот объединенный словарь возвращается. Рассмотрим примеры подобных объединений. Как paбoтaют вспомогательные функцииТеперь, когда у нас имеются определения вспомогательных функций, попробуем поработать с ними в интерактивной оболочке, чтобы лучше понять их специфику. Создадим пересечение словарей для шифротекста 'OLQIHXIRCKGNZ PLQRZKBZB MPBKSSIPLC' содержащего три шифрослова. Для этого потребуется создать по одному словарю для каждого слова, а затем объединить словари. Импортируем модуль simpleSubHacker.py в интерактивной оболочке, затем вызовем функцию getBlankCipherletterMapping() для создания пустого словаря шифробукв и сохраним его в переменной letterMappingl . Приступим к взлому первого шифрослова, 'OLQIHXIRCKGNZ'. Прежде всего, нужно получить для него шаблон, вызвав функцию getWordPattern(). Чтобы определить, какие слова, содержащиеся в файле английского словаря, соответствуют шаблону 0.1.2.3.4.5.3.6.7.8.9.10.11 (т.е. являются словами-кандидатами шифрослова 'OLQIHXIRCKGNZ'), импортируем модуль wordPatterns и выполним поиск этого шаблона. Шаблону шифрослова 'OLQIHXIRCKGNZ' соответствуют только два английских слова: 'UNCOMFORTABLE' и 'UNCOMFORTABLY'. Они являются нашими кандидатами, поэтому сохраним их в переменной candidates в виде списка. Далее следует сопоставить буквы этих слов с буквами шифрослова, используя функцию addLettersToMapping(). Сначала сопоставим слово 'UNCOMFORTABLE', обратившись к первому элементу списка кандидатов. >>> simpleSubHacker.addLettersToМapping(letterМappingl, ' OLQIВXIRCКGNZ', candidates[0]) >>> letterМappingl { 'А':[], 'В':[], 'С':['Т'], 'D':[], 'Е':[], 'F':[], 'G':['В'], 'Н':['М'], 'I':['О'], 'J':[], 'К':['А'], 'L':['N'], 'М':[], 'N':['L'], 'О':['U'], 'Р':[], 'Q':['С'], 'R':['R'], 'S':[], 'Т':[], 'U':[], 'V':[], 'W':[], 'Х':['F'], 'Y':[], 'Z':['Е'] } Как показывает анализ словаря letterMappingl, буквы шифрослова 'OLQIHXIRCKGNZ' транслируются в буквы слова 'UNCOMFORTABLE' следующим образом: 'О' - ['U'], 'L' - ['N'], 'Q' - ['С'] и т.д. Шифрослово 'OLQIHXIRCKGNZ' может быть также дешифровано в слово 'UNCOMFORTABLY', необходимо и его добавить в словарь. Введите в интерактивной оболочке следующие инструкции. >>> simpleSubHacker.addLettersToМapping (letterМappinql, ' OLQIВXIRCКGNZ', candidates[1]) > > > letterМappingl { 'А':[], 'В':[], 'С':['Т'], 'D':[], 'Е':[], 'F':[], 'G':['В'], 'Н':['М'], 'I':['О'], 'J':[], 'К':['А'], 'L':['N'], 'М':[], 'N':['L'], 'О':['U'], 'Р':[], 'Q':['С'], 'R':['R'], 'S':[], 'Т':[], 'U':[], 'V':[], 'W':[], 'Х':['F'], 'Y':[], 'Z':['Е', 'Y'] } Обратите внимание на то, что содержимое словаря letterMappingl почти не изменилось, за исключением того, что теперь шифробукве 'Z' соответствует не только буква 'Е', но и 'Y'. Это объясняется тем, что функция addLettersToMapping() добавляет букву в список только в том случае, если до этого она в нем отсутствовала. Итак, мы получили словарь шифробукв для первого из трех шифрослов. Теперь необходимо построить словарь для второго шифрослова, 'PLQRZKBZB', повторив весь процесс. >>> letterМappinq2 = simpleSubHacker.getblankCipherletterМapping() >>> wordPat = makeWordPatterns.getWordPattern('PLQRZКВZB') > > > candidates = wordPatterns.allPatterns [wordPat] >>> candidates ['CONVERSES', 'INCREASES', 'PORTENDED', 'UNIVERSES'] > > > for candidate in candidates: simpleSubHacker.addLettersToМapping(letterМapping2, 'PLQRZКВZB', candidate) > > > letterМapping2 { 'А':[], 'В':['S', 'D'], 'С':[], 'D':[], 'Е':[], 'F':[], 'G':[], 'Н':[], 'I':[], 'J':[], 'К':['R', 'А', 'N'], 'L':['О', 'N'], 'М':[], 'N':[], 'О':[], 'Р':['С', 'I', 'Р', 'U'], 'Q':['N', 'С', 'R', 'I'], 'R':['V', 'R', 'Т'], 'S':[], 'Т':[], 'U':[], 'V':[], 'W':[], 'Х':[], 'Y':[], 'Z':['Е'] } Вместо того чтобы четырежды вводить функцию addLettersToMapping() для каждого из четырех слов-кандидатов, мы организуем цикл for по словам-кандидатам в списке. Таким образом, мы получили словарь шифробукв для второго шифрослова. Следующий шаг - создание пересечения словарей, сохраненных в переменных letterMappingl и letterMapping2. Для этого мы передаем словари в функцию intersectMappings(). Введите в интерактивной оболочке следующие инструкции. >>> intersectedМapping = simpleSubHacker.intersectмappings( letterМappingl, letterмapping2) >>> intersectec:Mapping { 'А':[], 'В':['S', 'D'], 'С':['Т'], 'D':[], 'Е':[], 'F':[], 'G':['В'], 'Н':['М'], 'I':['О'], 'J':[], 'К':['А'], 'L':['N'], 'М':[], 'N':['L'], 'О':['U'], 'Р':['С', 'I', 'Р', 'U'], 'Q':['С'], 'R':['R'], 'S':[], 'Т':[], 'U':[], 'V':[], 'W':[], 'Х':['F'], 'Y':[], 'Z':['Е'] } Теперь список вариантов дешифрования для любой шифробуквы, входящей в объединенный словарь, должен содержать лишь варианты, общие для словарей letterMappingl и letterMapping2. Например, список для ключа 'Z' включает лишь одну букву ['Е'], поскольку словарь letterMappingl содержал список ['Е', 'Y'], тогда как словарь letterMapping2 - только список ['Е']. Далее повторяем все то же самое для третьего шифрослова, 'MPBKSSIPLC'. >>> letterМappingЗ = simpleSubHacker.getblankCipherletterмapping() >>> wordPat = makeWordPatterns.getWordPattern('МPBКSSIPLC') >>> candidates = wordPatterns.allPatterns[wordPat] >>> for i in range(len(candidates)): simpleSubHacker.addLettersToМapping(letterNapping3, 'МPВKSSIPLC', candidates[I]) >>> letterМappingЗ ( 'А':[], 'В':['М','S'], 'С':['Y', 'Т'], 'D':[], 'Е':[], 'F':[], 'G':[], 'Н':[], 'I':['Е', 'О'], 'J':[], 'К':['I', 'А'],'L':['L', 'N'], 'М':['А', 'D'], 'N':[], 'О':[], 'Р':['D', 'I'], 'Q':[], 'R':[], 'S':['Т', 'Р'], 'Т':[], 'U':[], 'V':[], 'W':[], 'Х':[], 'Y':[], 'Z':[] } ведите в интерактивной оболочке следующие инструкции, чтобы создать пересечение словарей letterMapping3 и intersectedMapping (последний представляет собой пересечение словарей letterMappingl и letterMapping2). >>> intersecteclМapping = simpleSubHacker.intersectmappings( letterМapping3, intersecteclМapping) >>> intersecteclМapping { 'А':[], 'В':['S'], 'С':['Т'], 'D':[], 'Е':[], 'F':[], 'G':['В'], 'Н':['М'], 'I':['О'], 'J':[], 'К':['А'], 'L':['N'], 'М':['А', 'D'], 'N':['L'], 'О':['U'], 'Р':['I'], 'Q':['С'], 'R':['R'], 'S':['Т', 'Р'], 'Т':[], 'U':[ ], 'V':[], 'W':[], 'Х':['F'], 'Y':[], 'Z':['Е'] } В этом примере можно найти решения лишь для ключей, в списке которых содержится единственное значение. Шифробуква 'К' дешифруется в 'А'. Обратите внимание ключ 'М' может быть дешифрован либо в 'А', либо в 'D'. Известно, что 'К' дешифруется в 'А', можно сделать вывод, что ключ 'М' должен дешифроваться в 'D', а не в 'А'. Если достоверно известно, что определенная буква уже используется для какой-то шифробуквы, то она не может быть использована для другой шифробуквы, поскольку такова особенность шифра простой замены. Рассмотрим, как функция removeSolvedLettersFromMapping() находит такие буквы и удаляет их из списков вариантов дешифрования. Нам понадобится только что созданный объединенный словарь intersectedMapping. Вычисление достоверно установленных букв в словаряхФункция removeSolvedLettersFromМapping() ищет в параметре letterMapping шифробуквы, имеющие лишь один вариант дешифрования. Такие шифробуквы считаются достоверно установленными, а это значит, никакие другие шифробуквы не могут содержать в своих списках данные варианты дешифрования. Это может вызвать цепную реакцию, ведь если буква исключается из списка вариантов дешифрования, содержащего всего два элемента, то мы получаем новую достоверно дешифрованную шифробукву. Функция обрабатывает подобные ситуации в цикле, удаляя из словаря достоверно установленные буквы. Код (Python): def removeSolvedLettersFrornМapping(letterMapping): . . . loopAgain = True while loopAgain: # Сначала предполагаем, что цикл не будет выполнен повторно loopAgain = False Не забывайте, что в параметре letterMapping передается ссылка на словарь, поэтому все изменения, внесенные в словарь внутри функции, сохранятся и после ее завершения. Создается переменная loopAgain, хранящая булево значение, которое определяет, должна ли функция пройти цикл повторно, если будет найдена еще одна достоверно установленная буква. Функция входит в цикл while. В начале цикла переменная loopAgain устанавливается равной False. Тем самым предполагается, что данная итерация станет последней. Переменная loopAgain будет установлена в True только в том случае, если в ходе итерации программа найдет новую шифробукву с достоверно установленным вариантом дешифрования. Далее определяются шифробуквы, для которых имеется ровно один вариант дешифрования. Такие варианты дешифрования будут удаляться из словаря. Код (Python): # solvedLetters - список букв в верхнем регистре, имеющих # единственное соответствие в словаре letterMapping solvedLetters = [ ] for cipherletter in LETTERS: if len(letterMapping[cipherletter]) == 1 : solvedLetters.append(letterMapping[cipherletter][0] ) Организуется цикл for по всем возможным 26 шифробуквам. Проверяются списки вариантов дешифрования для каждой шифробуквы (т.е. списки letterMapping[cipherletter]). Проверяется, не равна ли длина такого списка 1. Если это так, тогда для данной шифробуквы существует всего один вариант дешифрования, а значит, шифробуква дешифрована. Соответствующий вариант дешифрования добавляется в список solvedLetters. Этой букве всегда соответствует элемент letterMapping[cipherletter][0], так как в списке letterMapping[cipherletter] всего один элемент. По завершении цикла for переменная solvedLetters будет содержать список всех достоверно установленных вариантов дешифрования шифробукв. Далее функция проверяет, не числятся ли они в списках вариантов дешифрования других шифробукв, и удаляет их из этих списков. С этой целью организуется цикл for по всем 26 возможным шифробуквам для просмотра их списков. Окончание здесь
Шифр ЦезаряШифрование/расшифровка/взлом грубой силой и поиск дешифрованного варианта Код (Python): # шифрование/дешифрование шифра Цезаря. Взлом грубой силой и поиск дешифрованного варианта import re from math import log10 SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ ' class Caesar(object): def __init__(self,key): self.key = key % len(SYMBOLS) def remove_punctuation(self,text,filter='[^A-Z ]'): return re.sub(filter,'',text.upper()) def encipher(self,string): ret = '' for symbol in string: ret += SYMBOLS[(SYMBOLS.find(symbol) - self.key) % len(SYMBOLS)] return ret def decipher(self,string): ret = '' for symbol in string: ret += SYMBOLS[(SYMBOLS.find(symbol) + self.key) % len(SYMBOLS)] return ret class ngram_score(object): def __init__(self,ngramfile,sep=' '): #load a file containing ngrams and counts, calculate log probabilities ''' self.ngrams = {} file = open(ngramfile,encoding="UTF-8") for line in file: key,count = line.split(sep) self.ngrams[key] = int(count) self.L = len(key) self.N = sum(self.ngrams.values()) #calculate log probabilities for key in self.ngrams.keys(): self.ngrams[key] = log10(float(self.ngrams[key])/self.N) self.floor = log10(0.01/self.N) def score(self,text):# compute the score of text score = 0 ngrams = self.ngrams.__getitem__ for i in range(len(text)-self.L+1): if text[i:i+self.L] in self.ngrams: score += ngrams(text[i:i+self.L]) else: score += self.floor return score def main(): caes = Caesar(key=22) plaintext = 'THE CAESAR CIPHER IS ONE OF THE EARLIEST KNOWN AND SIMPLEST CIPHERS' plaintext = caes.remove_punctuation(plaintext) print("Исходный текст:\n" + plaintext) ciphertext = caes.encipher(plaintext) print("\nЗашифрованный текст:\n" + ciphertext) plaintext = caes.decipher(ciphertext) print("\nРасшифрованный текст:\n" + plaintext) print("\nВзлом шифра\n") fitness = ngram_score('english_quadgrams.txt') # загружаю файл со статистикой квадрограмм scores = [] #пустой список print('key',' '*25,'plaintext',' '*30,'fitness') print('-' * 78) for i in range(1,len(SYMBOLS)): text = re.sub('[^A-Z]','',Caesar(i).decipher(ciphertext)) x = fitness.score(text) scores.append((x,i)) print('%2s %s %.2f' % (i,Caesar(i).decipher(ciphertext),x)) res = sorted(scores, reverse=True, key=lambda x: x[0]) max_key = max(scores) print("\n",max_key[1]) print(Caesar(max_key[1]).decipher(ciphertext)) # Функция main() вызывается только в том случае, если файл Caesar.py был # запущен как программа, а не импортируется в виде модуля другой программой if __name__ == '__main__': main() Для дешифровки использовалась статистика по английским квадрограммам
Шифр Цезаря взлом с использованием словаря с английскими словамиШифрование/расшифровка/взлом зашифрованного файла. Для взлома использовался словарь с английскими словами, на экран выводились фрагменты с наилучшей статистикой Код (Python): import re SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ \n:.,"?-' class Caesar(object): def __init__(self,key): self.key = key % len(SYMBOLS) def encipher(self,string): ret = '' for symbol in string: ret += SYMBOLS[(SYMBOLS.find(symbol) - self.key) % len(SYMBOLS)] return ret def decipher(self,string): ret = '' for symbol in string: ret += SYMBOLS[(SYMBOLS.find(symbol) + self.key) % len(SYMBOLS)] return ret def remove_punctuation(text,filter='[^A-Z \n:.,"?-]'): return re.sub(filter,'',text.upper()) def loadDictionary(): dictionaryFile = open('english.txt') # получаем объект файла словаря englishWords = {} #создаем переменную - пустой словарь for word in dictionaryFile.read().split('\n'): englishWords[word] = None dictionaryFile.close() return englishWords # разбиваем с помощью split() переданную строку по переносам, возвращаем список ENGLISH_WORDS = loadDictionary() # функция loadDictionary() и возвращаемый ею словарь сохраняется в переменной def getEnglishCount(message): message = message.upper() message = remove_punctuation(message) #из строки удаляем неучтенные символы possibleWords = message.split() #разбиваем строку на слова и сохраняем в переменной if possibleWords == []: return 0.0 # слова отсутствуют, поэтому возвращаем 0.0. matches = 0 #для счетчика mаtсhеs устанавливается нулевое значение for word in possibleWords: #В цикле for мы проходим по всем словам в списке #possibleWords и проверяем существование каждого из них в словаре ENGLISH_WORDS if word in ENGLISH_WORDS: matches += 1 #Если слово содержится в словаре, значение счетчика #mаtсhеs инкрементируется return float(matches)/len(possibleWords) def main(): caes = Caesar(key=22) with open('Carroll.txt', 'r', encoding='utf-8') as file: plaintext = file.read() print("Исходный текст:\n",plaintext[:400]) plaintext = remove_punctuation(plaintext) ciphertext = caes.encipher(plaintext) print("\nЗашифрованный текст:\n",ciphertext[:400]) plaintext = caes.decipher(ciphertext) print("\nРасшифрованный текст:\n",plaintext[:400]) print("\nВзламываю шифр методом грубой силы:\n") englishPercentageOld = 25 for i in range(1,len(SYMBOLS)): text = Caesar(i).decipher(ciphertext) englishPercentage = round(getEnglishCount(text) * 100, 2) if englishPercentage > englishPercentageOld: print('Процент английских слов: %s%% Ключ #%2d: \n%s' % (englishPercentage, i, text)) englishPercentageOld = englishPercentage print("\nВзлом закончен") # Функция main() вызывается только в том случае, если файл Caesar.py был # запущен как программа, а не импортируется в виде модуля другой программой if __name__ == '__main__': main()
Взлом шифра простой замены с использованием словарного шаблона (Окончание) Код (Python): for cipherletter in LETTERS: for s in solvedLetters: if len(letterMapping[cipherletter]) != 1 and s in letterMapping[cipherletter]: letterMapping[cipherletter].remove(s) if len(letterMapping[cipherletter]) == 1 : # Решена новая буква, продолжаем цикл loopAgain = True return letterMapping Для каждой шифробуквы организуется циклический просмотр всех букв в списке solvedLetters с целью проверки того, не встречаются ли они в списке вариантов дешифрования letterMapping[cipherletter]. Проверяется как выполнение условия len(letterMapping[cipherletter]) != 1 (т.е. шифробуква еще не дешифрована), так и наличие достоверно установленной буквы в списке вариантов дешифрования данной шифробуквы. В случае выполнения обоих критериев достоверно установленная буква s удаляется из списка вариантов дешифрования. Если в результате такого удаления в списке вариантов дешифрования остается только одна буква, то значение переменной loopAgain устанавливается равным True, и тем самым функция сможет удалить эту букву из других списков на следующей итерации цикла. Как только на очередной итерации для переменной loopAgain не будет установлено значение True, цикл while, завершится, и функция вернет словарь letterMapping. Теперь переменная letterMapping будет содержать частично или полностью решенный словарь шифробукв. Код (Python): # Программа взлома шифра простой замены с использованием словарного шаблона import re, copy, wordPatterns LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' Key = 'TBMKLQOVECINHWRDAUPSGFJYXZ' def main(): with open('Carroll.txt','r',encoding='utf-8') as file: plaintext = file.read().upper() message = encryptMessage(Key, plaintext) # Определяем варианты дешифрования шифротекста print('Взламываю...') letterMapping = hackSimpleSub(message.upper()) # Выводим результаты print('Взломанное сообщение:') hackedMessage = decryptWithCipherletterMapping(message, letterMapping) print(hackedMessage) def getWordPattern(word): # Возвращает шаблон заданного слова, например '0.1.2.3.4.1.2.3.5.6' для слова 'DUSTBUSTER' word = word.upper() nextNum = 0 letterNums = {} wordPattern = [] for letter in word: if letter not in letterNums: letterNums[letter] = str(nextNum) nextNum += 1 wordPattern.append(letterNums[letter]) return '.'.join(wordPattern) def encryptMessage(key, message):#Функция предназначена как для шифрования сообщений translated = '' for symbol in message.upper():# Цикл по всем символам сообщения # На каждой итерации цикла for переменной symbol присваивается # очередной символ строки сообщения if symbol in LETTERS: # Шифрование символа symIndex = LETTERS.find(symbol) translated += key[symIndex] else:# Символ отсутствует в наборе LETTERS; просто добавляем его translated += symbol return translated def decryptMessage(key, message):#Функция для дешифрования сообщений translated = '' for symbol in message.upper(): # Цикл по всем символам сообщения # На каждой итерации цикла for переменной symbol присваивается # очередной символ строки сообщения if symbol in key: # Дешифрование символа symIndex = key.find(symbol) translated += LETTERS[symIndex] else: # Символ отсутствует в LETTERS; просто добавляем его translated += symbol return translated def getBlankCipherletterMapping(): # возвращает словарь, ключами которого являются односимвольные строки, # соответствующие 26 буквам алфавита. return {'A':[],'B':[],'C':[],'D':[],'E':[],'F':[],'G':[],'H':[],'I':[], 'J':[],'K':[],'L':[],'M':[], 'N':[],'O':[],'P':[],'Q':[],'R':[],'S':[],'T':[],'U':[],'V':[],'W':[],'X':[],'Y':[],'Z':[]} # функция addLettersToMapping, предназначенная для добавления букв в словарь def addLettersToMapping(letterMapping, cipherword, candidate): # Параметр letterMapping - это дешифровальный словарь, обрабатываемый # данной функцией. Параметр cipherword - это строковое значение шифрослова. # Параметр candidate - это английское слово-кандидат, в которое может быть # дешифровано данное шифрослово. Функция добавляет в дешифровальный словарь # буквы слова - кандидата в качестве вариантов дешифрования шифробукв for i in range(len(cipherword)): if candidate[i] not in letterMapping[cipherword[i]]: letterMapping[cipherword[i]].append(candidate[i]) def intersectMappings(mapA, mapB): # Чтобы построить пересечение словарей, создаем пустой словарь и # добавляем в него только варианты, существующие в ОБОИХ словарях intersectedMapping = getBlankCipherletterMapping() for letter in LETTERS: # Пустой список означает "возможна любая буква"; # в этом случае копируем список целиком if mapA[letter] == []: intersectedMapping[letter] = copy.deepcopy(mapB[letter]) elif mapB[letter] == []: intersectedMapping[letter] = copy.deepcopy(mapA[letter]) else: # Если буква из словаря mapA[letter] существует в словаре # mapB[letter], добавляем ее в intersectedМapping[letter] for mappedLetter in mapA[letter]: if mappedLetter in mapB[letter]: intersectedMapping[letter].append(mappedLetter) return intersectedMapping def removeSolvedLettersFromMapping(letterMapping):# Шифробуквы, транслируемые # только в одну букву, считаются дешифрованными, и соответствующие буквы могут # быть удалены из остальных списков. Например, если 'А' транслируется в ['М', 'N'], а # 'В' - в ['N'], мы знаем, что 'В' соответствует 'N', поэтому 'N' можно удалить из списка # для 'А'. Это также означает, что 'А' транслируется в ['М']. А поскольку 'А' теперь # соответствует единственной букве, можно удалить 'М' из остальных списков (вот # почему словарь сокращается в цикле) loopAgain = True while loopAgain: # Сначала предполагаем, что цикл не будет выполнен повторно loopAgain = False # solvedLetters - список букв в верхнем регистре, имеющих # единственное соответствие в словаре letterMapping solvedLetters = [] for cipherletter in LETTERS: if len(letterMapping[cipherletter]) == 1: solvedLetters.append(letterMapping[cipherletter][0]) # Если буква установлена, то она не может быть вариантом дешифрования # для другой шифробуквы, поэтому она должна быть удалена из других списков for cipherletter in LETTERS: for s in solvedLetters: if len(letterMapping[cipherletter]) != 1 and s in letterMapping[cipherletter]: letterMapping[cipherletter].remove(s) if len(letterMapping[cipherletter]) == 1: # Дешифрована новая буква, продолжаем цикл loopAgain = True return letterMapping def hackSimpleSub(message): intersectedMap = getBlankCipherletterMapping() cipherwordList = re.compile('[^A-Z ]').sub('', message.upper()).split() for cipherword in cipherwordList: # Получить новый дешифровальный словарь для # каждого шифрослова создается новый пустой словарь шифробукв, который # сохраняется в переменной candidateMap candidateMap = getBlankCipherletterMapping() # Чтобы найти слова-кандидаты для текущего шифрослова, вызываем функцию # getWordPattern() wordPattern = getWordPattern(cipherword) if wordPattern not in wordPatterns.allPatterns: continue # этого слова нет в словаре, продолжаем # Добавить буквы каждого слова-кандидата в словарь for candidate in wordPatterns.allPatterns[wordPattern]: addLettersToMapping(candidateMap, cipherword, candidate) # Найти пересечение нового словаря с существующим пересечением intersectedMap = intersectMappings(intersectedMap, candidateMap) # Удалить решенные буквы из других списков return removeSolvedLettersFromMapping(intersectedMap) # Обработанный словарь шифробукв, полученный в результате выполнения # функции removeSolvedLettersFromМapping(), возвращается функцией # hackSimpleSub() def decryptWithCipherletterMapping(ciphertext, letterMapping): # Возвращает строку шифротекста, дешифрованную с помощью словаря # шифробукв, в которой неоднозначности заменены подчеркиваниями # Сначала создаем ключ на основе словаря letterMapping key = ['x'] * len(LETTERS) for cipherletter in LETTERS: if len(letterMapping[cipherletter]) == 1: # Если имеется только одна буква, добавляем ее в ключ keyIndex = LETTERS.find(letterMapping[cipherletter][0]) key[keyIndex] = cipherletter else: ciphertext = ciphertext.replace(cipherletter, '*') # вместо нерасшифрованных букв стоит '*' key = ''.join(key) # Дешифруем шифротекст с помощью созданного ключа return decryptMessage(key, ciphertext) # Функция main() вызывается только в том случае, если файл simpleSubHacker.py # был запущен как программа, а не импортируется в виде модуля другой программой if __name__ == '__main__': main()
Шифр простой заменыШифр простой замены, простой подстановочный шифр, моноалфавитный шифр — класс методов шифрования, которые сводятся к созданию по определённому алгоритму таблицы шифрования, в которой для каждой буквы открытого текста существует единственная сопоставленная ей буква шифр-текста. Само шифрование заключается в замене букв согласно таблице. Для расшифровки достаточно иметь ту же таблицу, либо знать алгоритм, по которому она генерируется.Шифрование/расшифровка Код (Python): # Шифр простой замены LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ ' def main(): with open ('Carroll.txt',encoding="UTF-8") as file: plaintext = file.read() myKey = 'TBMK LQOVECINHWRDAUPSGFJYXZ' print('Исходный текст:') print(plaintext[:200]) plaintext = plaintext.replace('\n','Z') print('Шифрую файл с ключом '+myKey) translated = encryptMessage(myKey, plaintext) print('Зашифрованный текст:\n'+translated[:200]) plaintext = decryptMessage(myKey, translated) plaintext = plaintext.replace('Z','\n') print('Расшифрованный текст:\n' + plaintext) def encryptMessage(key, message): translated = '' # Цикл по всем символам сообщения for symbol in message.upper(): # На каждой итерации цикла for переменной symbol присваивается # очередной символ строки сообщения if symbol in LETTERS: # Шифрование символа symIndex = LETTERS.find(symbol) translated += key[symIndex] return translated def decryptMessage(key, message): translated = '' for symbol in message.upper(): if symbol in key: # Дешифрование символа symIndex = key.find(symbol) translated += LETTERS[symIndex] return translated #Функция main() вызывается только в том случае, если файл simplSubCipher.py был #запущен как программа, а не импортируется в виде модуля другой программой if __name__ == '__main__': main()
Шифр ВиженэраШифрование/расшифровка/взлом Код (Python): SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ \n,.:()-' a = [] import re def lenKey(txt): #возвращает длину ключа, иначе выводим на экран [index_list] index_list = [] # массив индекса совпадений lenMsg = len(txt) shift_txt = txt[2:] + txt[:2]# 2 - минимальный размер ключа for shift in range(2, len(txt)//10): count = 0 # число совпавших символов(с одинаковым индексом) изначальной строки со сдвинутой на shift символов for i in range(lenMsg): if txt[i] == shift_txt[i]: count += 1 index_list.append(round(count * 100 / lenMsg, 3)) # добавляем индекс сопадений в масив shift_txt = shift_txt[1:] + shift_txt[:1] # снова двигаем строку на 1 символ inx = [] # индексы больших "индексов совпадений" в [index_list] for i in range(1, len(index_list)-1): if index_list[i-1] < index_list[i] > index_list[i+1] and index_list[i] > 4.4: # определяем скачек 4,4? inx.append(index_list.index(index_list[i]) + 2) # записываем на каких по счету сдвигах был скачек if inx[1] - inx[0] == inx[2] - inx[1]: # этого условия достаточно в большинстве случаев return inx[1] - inx[0] # вернет число кратное длине ключа def textFormat(txt, lenKey):#Принимает исходный текст и длину ключа. Разбивает текст на lenKey элементов l = [''] * lenKey for i in range(0, len(txt), lenKey): split_txt = txt[i:i + lenKey] try: for q in range(lenKey): l[q] += split_txt[q] except IndexError: None return l#Возвращает список из lenKey строк def letterFrequency(line):#Принимает строку. Возвращает массив с числом(не путать с частотой) #символов для каждого символа алфавита #[8,13,7,7,0,2,0,4,1,0,0,0,2,2,0,2,29,13,4,9,9,7,16,3,2,17,1,8,10,10,14,22,4] letterFrequencyList = [0] * len(SYMBOLS) for letterIndex, letter in enumerate(SYMBOLS): letterFrequencyList[letterIndex] = line.count(letter) return letterFrequencyList def caesar(txt, _step): ret = '' for symbol in txt: symbolIndex = SYMBOLS.find(symbol) ret += SYMBOLS[(symbolIndex + _step) % len(SYMBOLS)] return ret def matchIndex(letter_count1, letter_count2, num_line, step=0): #На вход подается массивы частот символов 0-го и n-го столбца. #На выходе получаем сдвиг n-го столбца относительно 0-го res = 0 for i in range(len(SYMBOLS)): res += letter_count1[i] * letter_count2[i] if 0.064 <= round(res/(len(lines[0])*len(lines[num_line])), 3) <= 0.1:#индекс совпадения для английского языка 0,0644 0,0667 return step # вернет нужный сдвиг new_letter_count2 = letterFrequency(caesar(lines[num_line], step+1)) return matchIndex(letter_count1, new_letter_count2, num_line, step+1) def findKey(lines): letter_counts = [] for line in lines: letter_counts.append(letterFrequency(line)) keys = [0] * (len(lines) - 1) for i in range(0, len(lines)-1): keys[i] = matchIndex(letter_counts[0], letter_counts[i + 1], i + 1) for indexLetter in range(len(SYMBOLS)): word = SYMBOLS[indexLetter] for key in keys: word += SYMBOLS[indexLetter - key] a.append(word) def a2i(ch): arr = dict([(SYMBOLS[i],i) for i in range(len(SYMBOLS))]) return arr[ch] def i2a(i): return SYMBOLS[i % len(SYMBOLS)] def decipher(string,key): ret = "" for (i, c) in enumerate(string): i = i%len(key)#ключ растянут по длине зашифрованного текста ret += i2a(a2i(c) - a2i(key[i])) return ret def encipher(string,key): ret = '' for (i,c) in enumerate(string): i = i%len(key) ret += i2a(a2i(c) + a2i(key[i])) return ret def remove_punctuation(text,filter='[^A-Z \n,.:()-]'): return re.sub(filter,'',text.upper()) def loadDictionary(): dictionaryFile = open('english.txt') englishWords = {} for word in dictionaryFile.read().split('\n'): englishWords[word] = None dictionaryFile.close() return englishWords ENGLISH_WORDS = loadDictionary() def getEnglishCount(message): message = message.upper() message = remove_punctuation(message) possibleWords = message.split() if possibleWords == []: return 0.0 # No words at all, so return 0.0. matches = 0 for word in possibleWords: if word in ENGLISH_WORDS: matches += 1 return float(matches) / len(possibleWords) if __name__ == '__main__': key = 'HAPPY' with open('Carroll.txt', 'r', encoding='utf-8') as file: plaintext = file.read() plaintext = remove_punctuation(plaintext) print("Исходный текст:\n" + plaintext[:100]) ciphertext = encipher(plaintext,key) print("\nКлюч:"+key+"\nЗашифрованный текст:\n" + ciphertext[:100]) plaintext = decipher(ciphertext,key) print("\nРасшифрованный текст:\n" + plaintext[:100]) print('Взламываю...') # Работу программы на Python можно в любой момент прервать, нажав <Ctrl+C> print('(Нажми Ctrl-C для выхода из программы.)') print('Вычисляю длину ключа') lnKey = lenKey(ciphertext) # вычисляем предполагаемую длину ключа print("Предполагаемая длина ключа: ", lnKey) lines = textFormat(ciphertext, lnKey) # разбиваем text на lenKey частей и делаем из них массив lines findKey(lines) # находим сдвиги всех столбцов относительно первого englishPercentageOld = 0 for i in range(len(SYMBOLS)): decryptedText = decipher(ciphertext, a[i]) englishPercentage = round(getEnglishCount(decryptedText) * 100, 2) if englishPercentage > englishPercentageOld: print('Процент английских слов: %5s%% Ключ:%s' % (englishPercentage,a[i])) englishPercentageOld = englishPercentage if englishPercentage > 40: print('Возможный результат дешифровки:\n' + decryptedText) print('\nВзлом закончен ')
Шифр простой замены Взлом с использованием статистики английских квадрограммШифрование/расшифровка/для взлома используем статистику английских квадрограмм и частотный анализ латинских букв в английском языке Код (Python): import random, re from math import log10 LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ ' FREQ_LETTERS = ' ETAOINSHRDLCUMWFGYPBVKXJQZ' etalon_dict = {' ':14.10,'E':12.10,'T':8.94,'A':8.55,'O':7.47,'I':7.33,'N':7.17, 'S':6.73,'H':4.96,'R':6.33,'D':3.87,'L':4.21,'C':3.16,'U':2.68, 'M':2.53,'W':1.83,'F':2.18,'G':2.09,'Y':1.72,'P':2.07,'B':1.60, 'V':1.06,'K':0.81,'X':0.19,'J':0.22,'Q':0.11,'Z':0.09} def remove_punctuation(text,filter='[^A-Z ]'): return re.sub(filter,'',text.upper()) def main(): fitness = ngram_score('english_quadgrams.txt') # загружаю статистику по квадраграммам with open('Carroll.txt',encoding="UTF-8") as file: plaintext = file.read() print("Исходный текст:\n" + plaintext[:200]) plaintext = plaintext.replace('\n','z') plaintext = remove_punctuation(plaintext) myKey = 'TBMK LQOVECINHWRDAUPSGFJYXZ' print('Ключ шифрования:"'+myKey+'"') print('Алфавит:\t'+'"'+LETTERS+'"') ss = SimpleSubstitution(myKey) ciphertext = ss.encipher(plaintext) print("Зашифрованный текст:\n" + ciphertext[:200]) plaintext = ss.decipher(ciphertext) plaintext = plaintext.replace('Z','\n') print("Расшифрованный текст:\n" + plaintext[:340]) print('Взламываю...') # Работу программы на Python можно в любой момент прервать, нажав <Ctrl+C> print('(Нажми Ctrl-C для выхода из программы.)') code = list(ciphertext.upper()) alphabet = {} for i in LETTERS: alphabet[i]=0 total = 0 for i in range(len(code)): if code[i] in alphabet: alphabet[code[i]] += 1 total += 1 sorted_alphabet = dict(sorted(alphabet.items(), key=lambda item: item[1], reverse=True)) for k, v in sorted_alphabet.copy().items(): sorted_alphabet[k] = round(v*100/total,3) sorted_alphabet_keys = list(sorted_alphabet.keys()) sorted_alphabet_values = list(sorted_alphabet.values()) etalon_dict_keys = list(etalon_dict.keys()) etalon_dict_values = list(etalon_dict.values()) print('Начинаем расшифровку\n Эталон \u2502Частотность в тексте') print('\u2500'*2+'\u253C'+'\u2500'*9+'\u253C'+'\u2500'*9) for i in range(len(LETTERS)): print("%2d\u2502%c: %5.2f \u2502 %c: %5.2f" % (i,etalon_dict_keys[i],etalon_dict_values[i],sorted_alphabet_keys[i],sorted_alphabet_values[i])) ETALON = 'x' * len(LETTERS) for i in range(len(LETTERS)): index = LETTERS.find(etalon_dict_keys[i]) ETALON = ETALON[:index] + sorted_alphabet_keys[i].upper()+ ETALON[index+1:] print('алфавит:\t',LETTERS) print('Возможный ключ:\t',ETALON) maxkey = list(ETALON) maxscore = -99e9 parentscore,parentkey = maxscore,maxkey[:] print('Взламываем шифр простой замены, возможно, придется подождать') print('несколько итераций для правильного результата.') print('Если хотите завершить расчет \u2500 тогда нажмите CTRL+C') # продолжаем, пока процесс не прибьет пользователь i = 0 while 1: i = i+1 deciphered = SimpleSubstitution(parentkey).decipher(ciphertext) parentscore = fitness.score(deciphered) count = 0 while count < 1000: a = random.randint(0,len(LETTERS)-1) b = random.randint(0,len(LETTERS)-1) child = parentkey[:] # поменять местами два символа в child child[a],child[b] = child[b],child[a] deciphered = SimpleSubstitution(child).decipher(ciphertext) score = fitness.score(re.sub('[^A-Z]','',deciphered)) # если child был лучше, замените им parent if score > parentscore: parentscore = score parentkey = child[:] count = 0 count = count+1 # keep track of best score seen so far if parentscore > maxscore: maxscore,maxkey = parentscore,parentkey[:] print('лучший результат на данный момент:',maxscore,'на итерации',i) ss = SimpleSubstitution(maxkey) print('алфавит:\t',LETTERS) print('лучший ключ:\t',''.join(maxkey)) plaintext = ss.decipher(ciphertext) plaintext = plaintext.replace('Z','\n') print('открытый текст:\n' + plaintext[:340]) break print('Взлом завершен') class ngram_score(object): def __init__(self,ngramfile,sep=' '): ''' load a file containing ngrams and counts, calculate log probabilities ''' self.ngrams = {} fileobj = open('english_quadgrams.txt',encoding="UTF-8") for line in fileobj: key,count = line.split(sep) self.ngrams[key] = int(count) self.L = len(key) self.N = sum(self.ngrams.values()) #calculate log probabilities for key in self.ngrams.keys(): self.ngrams[key] = log10(float(self.ngrams[key])/self.N) self.floor = log10(0.01/self.N) fileobj.close() def score(self,text): ''' compute the score of text ''' score = 0 ngrams = self.ngrams.__getitem__ for i in range(len(text)-self.L+1): if text[i:i+self.L] in self.ngrams: score += ngrams(text[i:i+self.L]) else: score += self.floor return score class SimpleSubstitution(object): def __init__(self,key): assert len(key) == len(LETTERS) self.key = [k.upper() for k in key] def encipher(self,string): ret = '' for symbol in string.upper():# Цикл по всем символам сообщения if symbol in LETTERS: # Шифрование символа symIndex = LETTERS.find(symbol) ret += self.key[symIndex] return ret def decipher(self,string): key = ''.join(self.key) ret = '' for symbol in string.upper(): if symbol in key: # Дешифрование символа symIndex = key.find(symbol) ret += LETTERS[symIndex] return ret #Функция main() вызывается только в том случае, если файл simplSubCipher.py был #запущен как программа, а не импортируется в виде модуля другой программой if __name__ == '__main__': main()
Шифр простой замены для взлома используем частотный анализ, словарь, логику и здравый смысл Код (Python): import random, re LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ ' etalon_dict = {' ':14.10, 'E':12.10, 'T':8.94, 'A':8.55, 'O':7.47, 'I':7.33, 'N':7.17, 'S':6.73, 'H':4.96, 'R':6.33,'D':3.87, 'L':4.21,'C':3.16,'U':2.68,'M':2.53,'W':1.83,'F':2.18,'G':2.09,'Y':1.72, 'P':2.07,'B':1.60,'V':1.06,'K':0.81,'X':0.19,'J':0.22,'Q':0.11,'Z':0.09} def remove_punctuation(text,filter='[^A-Z \n]'): return re.sub(filter,'',text.upper()) def loadDictionary(): with open('english.txt') as file: englishWords = file.read().split('\n') return englishWords class SimpleSubstitution(object): def __init__(self,key): assert len(key) == len(LETTERS) self.key = [k.upper() for k in key] def encipher(self,string): ret = '' for symbol in string.upper():# Цикл по всем символам сообщения if symbol in LETTERS: # Шифрование символа symIndex = LETTERS.find(symbol) ret += self.key[symIndex] else: ret += symbol return ret def decipher(self,string): key = ''.join(self.key) ret = '' for symbol in string.upper(): if symbol in key: # Дешифрование символа symIndex = key.find(symbol) ret += LETTERS[symIndex] else: ret += symbol return ret def getRandomKey():#функция возвращает подходящий ключ. Осуществляется случайное #перемешивание символов в константе LETTERS key = list(LETTERS) random.shuffle(key) return ''.join(key) def main(): with open('Carroll.txt',encoding="UTF-8") as file: plaintext = file.read() #print("Исходный текст:\n" + plaintext[:200]) plaintext = remove_punctuation(plaintext.lower()) ENGLISH_WORDS = loadDictionary()# вызывается функция loadDictionary() и # возвращаемый ею словарь сохраняется в переменной ENGLISH_WORDS. myKey = 'QBLSXICDOPNETZ YRHAWGUKMVFJ'#getRandomKey() print('Ключ шифрования: '+myKey) ss = SimpleSubstitution(myKey) ciphertext = ss.encipher(plaintext) ciphertext = ciphertext.lower() print('Взламываю...') code = list(ciphertext.upper()) alphabet = {} for i in LETTERS: alphabet[i]=0 total = 0 for i in range(len(code)): if code[i] in alphabet: alphabet[code[i]] += 1 total += 1 sorted_alphabet = dict(sorted(alphabet.items(), key=lambda item: item[1], reverse=True)) for k, v in sorted_alphabet.copy().items(): sorted_alphabet[k] = round(v*100/total,3) sorted_alphabet_keys = list(sorted_alphabet.keys()) sorted_alphabet_values = list(sorted_alphabet.values()) etalon_dict_keys = list(etalon_dict.keys()) etalon_dict_values = list(etalon_dict.values()) print('Начинаем расшифровку\n Эталон \u2502Частотность в тексте') print('\u2500'*2+'\u253C'+'\u2500'*9+'\u253C'+'\u2500'*9) for i in range(len(LETTERS)): print("%2d\u2502%c: %5.2f \u2502 %c: %5.2f" % (i,etalon_dict_keys[i],etalon_dict_values[i],sorted_alphabet_keys[i],sorted_alphabet_values[i])) #Функция main() вызывается только в том случае, если файл simplSubCipher.py был #запущен как программа, а не импортируется в виде модуля другой программой if __name__ == '__main__': main() Из индекса Фридмана [math]IC\approx 0.0644[/math] можно сделать вывод о том, что шифротекст на английском языке. Результат работы программы. В глаза должно бросится следующее — средняя длина английского слова 6-7 букв, шифрослова намного длиннее, отсюда вывод пробелу будет соответствовать самый часто встречающийся символ j=19.97 второй символ по частоте соответствует букве E в английских текстах x=10.05 Код (Python): ciphertext = ciphertext.replace(' ','f')# буква F в шифротексте не задействована, заменяем "лжепробел" на F ciphertext = ciphertext.replace('j',' ')# 1 восстанавливаем пробел ciphertext = ciphertext.replace('x','E')# 2 самая часто встречающаяся буква английского языка E message_list = ciphertext.split()# получаю список шифрослов разделенных реальными пробелами sorted_message = sorted(message_list, key=len) message_list= list(set(sorted_message)) # удаляю повторы''' print('однобуквенные слова в шифротексте') for word in message_list: if len(word)==1: print(word) print('однобуквенные слова в английском словаре') for word in ENGLISH_WORDS: if len(word)==1: print(word) Частота шифробуквы "o"=5.07, скорее всего это 'I'=7.33 Частота шифробуквы "q"=6.47, скорее всего это 'A'=8.55 Код (Python): ciphertext = ciphertext.replace('o','I')# 3 I ciphertext = ciphertext.replace('q','A')# 4 A Обратите внимание на шифрослово "bEcIzzIzc" Код (Python): for word in ENGLISH_WORDS: if len(word)==9 and word[1]=='E' and word[3]==word[6]=='I' and word[2]==word[8] and word[4]==word[5]==word[7]: print(word) шифрослову "bEcIzzIzc" соответствует английское слово "BEGINNING", так мы нашли еще 3 буквы "B", "G" и "N" Код (Python): ciphertext = ciphertext.replace('b','B')# 5 BEGINNING ciphertext = ciphertext.replace('c','G')# 6 ciphertext = ciphertext.replace('z','N')# 7 А дальше понеслось, заменяя одну шифробукву за другой, просматриваем шифротекст снова и снова, иногда делаем поиск в английском словаре. В конце концов находим полное соответствие 26 буквам латинского алфавита. Последние 3 буквы найдены при ответе на вопрос "А какие еще буквы не найдены в алфавите?" Код (Python): ciphertext = ciphertext.replace('s','D')# 8 AND ciphertext = ciphertext.replace('v','Y')# 9 BY ciphertext = ciphertext.replace('w','T')#10 GET ciphertext = ciphertext.replace('f','O')#11 INTO ciphertext = ciphertext.replace('d','H')#12 THE ciphertext = ciphertext.replace('u','V')#13 HAVING ciphertext = ciphertext.replace('h','R')#14 VERY ciphertext = ciphertext.replace('a','S')#15 SITTING SISTER ciphertext = ciphertext.replace('n','K')#16 BOOK ciphertext = ciphertext.replace('g','U')#17 OUT ciphertext = ciphertext.replace('e','L')#18 TUNNEL ciphertext = ciphertext.replace('l','C')#19 CONVERSATIONS ciphertext = ciphertext.replace('k','W')#20 TWICE WAS ciphertext = ciphertext.replace('y','P')#21 UP PICTURES ciphertext = ciphertext.replace('i','F')#22 OF AFTER FOR ciphertext = ciphertext.replace('t','M')#23 SOMEBODY MOMENT ciphertext = ciphertext.replace('p','J')#24 JAR JUST Общее время взлома около 20 минут
Шифр «Изгородь» (Rail-fence Cipher)Разновидность перестановочного шифра. Получил название из-за способа шифрования, напоминающего забор из горизонтальных (rail) и диагональных (fence) штакетин. Открытый текст записывается на пересечении диагональных и вертикальных штакетин сперва сверху-вниз на диагональных «штакетниках» воображаемого ограждения, затем, когда достигается нижний штакетник, текст пишется снизу-вверх, пока не достигается верхний штакетник, затем снова сверху-вниз, и так далее, пока не будет записан весь открытый текст. Затем зашифрованный текст считывается построчно. Шифруем текст "HELLO, WORLD!" ключ Nшифротекстдиагоналейпериод 2(N-1)2H.L.O.O.L. .E.L.W.R.DHLOOLELWRD923H...O...L. .E.L.W.R.D ..L...O...HOLELWRDLO544H.....O... .E...W.R.. ..L.O...L. ...L.....DHOEWRLOLLD365H.......L. .E.....R.D ..L...O... ...L.W.... ....O.....HLERDLOLWO386H......... .E.......D ..L.....L. ...L...R.. ....O.O... .....W....HEDLLLROOW27H......... .E........ ..L....... ...L.....D ....O...L. .....W.R.. ......O...HELLDOLWRO28H......... .E........ ..L....... ...L...... ....O..... .....W...D ......O.L. .......R..HELLOWDOLR29H......... .E........ ..L....... ...L...... ....O..... .....W.... ......O... .......R.D ........L.HELLOWORDL2При длине открытого текста в 10 символов, ключ может быть от 2 до 9.РасшифровкаПусть [math]N[/math] — количество строк, используемых при шифровании. При шифровании последовательность расположения каждой буквы меняется в зависимости от цикла. Если [math]N=2,3,4,5[/math], вертикальное расположение повторяется с периодом [math]2,4,6,8[/math]. Период «пилы» от верхнего пика до следующего верхнего пика составляет [math]2(N−1)[/math] символов. [math]L[/math] — длина строки, которую нужно расшифровать. строкаИндекс символа00102011911192122812182233713172344614165515 Текст разбивается на строки так, чтобы количество символов в первой и последней строке равно [math]\displaystyle{\frac{N}{2}}[/math], количество символов в каждой промежуточной строке [math]N-1[/math]. Шифруем текст "WE ARE DISCOVERED. RUN AT ONCE" [math]L=24[/math] при [math]N=6[/math], получим шифротекст: 6W.........V.........O..... .E.......O.E.......T.N.... ..A.....C...R.....A...C... ...R...S.....E...N.....E.. ....E.I.......D.U.......*. .....D.........R.........*WVOEOETNACRACRSENEEIDU*DR*Для упрощения зашифрования дополним исходный текст символами (*), которые будут удалены при расшифровке. Добавим переменные, [math]x+1 =[/math] количество диагоналей в зашифрованном тексте и [math]y[/math] количество пустых символов в последней диагонали. [math]\displaystyle 1={\frac {L+y}{N+(N-1)x}}[/math] Вычислим [math]x[/math] и [math]y[/math]. Увеличиваем [math]x[/math] на 1, пока знаменатель не станет больше, чем [math]L[/math], а затем найдем число [math]y[/math]. Пусть [math]L=24[/math] и [math]N=6[/math], решим уравнение [math]1=\displaystyle{\frac {24+y}{6+5x}}[/math] Упрощаем дробь [math]24+y=6+5x,\ [/math] [math]18+y=5x,\ [/math] [math]y\geqslant 0[/math] [math]18+y=5x[/math][math]x[/math][math]18+y>5[/math]1[math]18+y>10[/math]2[math]18+y>15[/math]3[math]18+y=20[/math]4При [math]N=6[/math] строк, [math]x+1=5 [/math] диагоналей, [math]y=2[/math] пустых символа в конце открытого текста. КриптоанализКлючом шифра является [math]N[/math], количество строк. Если [math]N[/math] известно, зашифрованный текст можно расшифровать с помощью приведённого выше алгоритма. Значения [math]N[/math] равные или превышающие [math]L[/math], длину зашифрованного текста, не подходят, так как в этом случае зашифрованный текст совпадает с исходным. Количество подходящих ключей невелико, это позволяет использовать метод перебора всех возможных ключей. В результате шифр «изгороди» считается слабым. Код (Python): import re SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ \n,.()?":-' class Railfence(object): def __init__(self, key): self.key = key assert 1 < self.key, 'неправильный ключ: key='+str(key)+', должен быть больше единицы' def encipher(self,string): return ''.join(self.buildfence(string, self.key)) def decipher(self,string): ind = range(len(string)) pos = self.buildfence(ind, self.key) return ''.join(string[pos.index(i)] for i in ind) def buildfence(self,chars, numrails): fence = [[None] * len(chars) for n in range(numrails)] rails = list(range(numrails - 1)) + list(range(numrails - 1, 0, -1)) for n, x in enumerate(chars): fence[rails[n % len(rails)]][n] = x return [c for rail in fence for c in rail if c is not None] def remove_punctuation(text,filter='[^A-Z \n,.()?":-]'): return re.sub(filter,'',text.upper()) def loadDictionary(): dictionaryFile = open('english.txt') # получаем объект файла словаря englishWords = {} #создается переменная englishWords в виде пустого словаря for word in dictionaryFile.read().split('\n'): englishWords[word] = None dictionaryFile.close() return englishWords #Строковый метод split() разбивает переданную ему строку по переносам, #возвращая список из нескольких строк ENGLISH_WORDS = loadDictionary() # вызывается функция loadDictionary() и возвращаемый ею словарь сохраняется # в переменной ENGLISH_WORDS. def getEnglishCount(message): message = message.upper() #символы строки преобразуются в верхний регистр message = remove_punctuation(message) #с помощью функции removeNonLetters() # из строки удаляются все небуквенные символы, числа и знаки препинания possibleWords = message.split() #метод split() разбивает строку на слова #и сохраняет их в переменной possibleWords if possibleWords == []: return 0.0 # слова отсутствуют, поэтому возвращаем 0.0. matches = 0 #для счетчика mаtсhеs устанавливается нулевое значение for word in possibleWords: #В цикле for мы проходим по всем словам в списке #possibleWords и проверяем существование каждого из них в словаре ENGLISH_WORDS if word in ENGLISH_WORDS: matches += 1 #Если слово содержится в словаре, значение счетчика #mаtсhеs инкрементируется return float(matches)/len(possibleWords) def main(): key = 22 rf = Railfence(key) with open('decrypted.txt','r',encoding = 'utf-8') as file: plaintext = file.read() plain_text = remove_punctuation(plaintext) print("Исходный текст:\n" + plaintext[:200]) ciphertext = rf.encipher(plaintext.upper()) print("\nЗашифрованный текст с ключом = "+str(key)+"\n" + ciphertext[:200]) plaintext = rf.decipher(ciphertext) print("\nРасшифрованный текст:\n" + plaintext[:200]) print("\nВзлом шифра...\nНажмите Ctrl+C для завершения программы") englishPercentageOld = 0 for key in range(2,len(ciphertext)+1): text = Railfence(key).decipher(ciphertext) englishPercentage = round(getEnglishCount(text) * 100, 2) if englishPercentage > englishPercentageOld: print('Процент английских слов: %s%% Ключ #%2d: \n%s' % (englishPercentage, key, text[:165])) englishPercentageOld = englishPercentage print('\nВзлом закончен') # Функция main() вызывается только в том случае, если файл railfence.py был # запущен как программа, а не импортируется в виде модуля другой программой if __name__ == '__main__': main()
Перестановочный шифр — вертикальная перестановкаМетод симметричного шифрования, в котором элементы исходного открытого текста меняют местами. Элементами текста могут быть отдельные символы, пары букв, тройки букв, комбинирование этих случаев. Шифры перестановки делят на два класса: Шифры одинарной (простой) перестановки — при шифровании символы открытого текста перемещаются с исходных позиций в новые один раз. Шифры множественной (сложной) перестановки — при шифровании символы открытого текста перемещаются с исходных позиций в новые несколько раз. Альтернатива шифрам перестановки — подстановочные шифры. В них элементы текста не меняют свою последовательность, а изменяются сами. Разновидность шифра перестановки — вертикальная перестановка. Используется прямоугольная таблица, в которую в верхнюю строку записывается ключ, а начиная со второй строки, открытое сообщение, которое записывается по строкам слева направо. Ключ = 'GERMAN', открытый текст = 'WE ARE DISCOVERED. RUN AT ONCE' GERMANWEXAREXDISCOVEREDXRUNXATXONCE_Выписывается шифрограмма по вертикалям, при этом столбцы выбираются в порядке, определяемом ключом. AEGMNRREWAEXCDXSOIDEVEXRAURXTNEOXC_Nшифротекст = RCDAEEDEUOWXVRXASEXCEOXT_XIRNNШифрация/дешифрация Код (Python): SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' import re def remove_punctuation(text,filter='[^A-Z]'): return re.sub(filter,'',text.upper()) class ColTrans(object): def __init__(self,keyword): self.keyword = keyword assert len(keyword)>0, 'недопустимое ключевое слово в init: должно быть >= 1' # возвращает отсортированные индексы ключевого слова,'GERMAN' = [2,1,5,3,0,4] def sortind(self,word): t1 = [(word[i],i) for i in range(len(word))] t2 = [(k[1],i) for i,k in enumerate(sorted(t1))] return [q[1] for q in sorted(t2)] # возвращает неотсортированные индексы ключевого слова,'GERMAN' = [0,1,2,3,4,5] def unsortind(self,word): t1 = [(word[i],i) for i in range(len(word))] return [q[1] for q in sorted(t1)] def encipher(self,string): string = string.replace('X','KS') if (len(string) % len(self.keyword)) != 0: for i in range(len(self.keyword)-(len(string) % len(self.keyword))): string += ' ' string = string.replace(' ','X') string = remove_punctuation(string) ret = '' ind = self.sortind(self.keyword) for i in range(len(self.keyword)): ret += string[ind.index(i)::len(self.keyword)] return ret # расшифровка может быть затруднена, так как столбцы могут быть невыровнены def decipher(self,string): L,M = len(string),len(self.keyword) ret = ['_']*L ind = self.unsortind(self.keyword) upto = 0 for i in range(len(self.keyword)): thiscollen = (int)(L/M) if ind[i]< L%M: thiscollen += 1 ret[ind[i]::M] = string[upto:upto+thiscollen] upto += thiscollen s = ''.join(ret) s = s.replace('X',' ') s = s.replace('KS','X') return s def main(): plaintext = 'WE ARE DISCOVERED. RUN AT ONCE' print("Исходный текст:\n" + plaintext) keyword = 'german' cl = ColTrans(keyword) ciphertext = cl.encipher(plaintext) print("\nЗашифрованный текст с ключом = ",keyword,"\n",ciphertext) plaintext = cl.decipher(ciphertext) print("\nРасшифрованный текст:\n",plaintext) # Функция main() вызывается только в том случае, если файл ColTrans.py был # запущен как программа, а не импортируется в виде модуля другой программой if __name__ == '__main__': main() Взлом перестановочного шифраПредполагаем, что длина ключа от 2 до 10 символов Код (Python): SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ \n,.()?":-' import re from itertools import permutations def remove_punctuation(text,filter='[^A-Z \n,.()?":-]'): return re.sub(filter,'',text.upper()) class ColTrans(object): def __init__(self,keyword): self.keyword = keyword assert len(keyword)>0, 'недопустимое ключевое слово в init: должно быть >= 1' # возвращает отсортированные индексы ключевого слова,'GERMAN' = [2,1,5,3,0,4] def sortind(self,word): t1 = [(word[i],i) for i in range(len(word))] t2 = [(k[1],i) for i,k in enumerate(sorted(t1))] return [q[1] for q in sorted(t2)] # возвращает неотсортированные индексы ключевого слова,'GERMAN' = [0,1,2,3,4,5] def unsortind(self,word): t1 = [(word[i],i) for i in range(len(word))] return [q[1] for q in sorted(t1)] def encipher(self,string): if (len(string) % len(self.keyword)) != 0: for i in range(len(self.keyword)-(len(string) % len(self.keyword))): string += ' ' string = remove_punctuation(string) ret = '' ind = self.sortind(self.keyword) for i in range(len(self.keyword)): ret += string[ind.index(i)::len(self.keyword)] return ret # расшифровка может быть затруднена, так как столбцы могут быть невыровнены def decipher(self,string): L,M = len(string),len(self.keyword) ret = ['_']*L ind = self.unsortind(self.keyword) upto = 0 for i in range(len(self.keyword)): thiscollen = (int)(L/M) if ind[i]< L%M: thiscollen += 1 ret[ind[i]::M] = string[upto:upto+thiscollen] upto += thiscollen s = ''.join(ret) return s def isEnglish(message, wordPercentage=80, letterPercentage=85): # По умолчанию 20% слов должны быть в файле словаря, # а 85% символов сообщения должны быть буквами или # пробелами (а не знаками препинания или числами) wordsMatch = getEnglishCount(message) * 100 >= wordPercentage numLetters = len(remove_punctuation(message)) messageLettersPercentage = float(numLetters) / len(message) * 100 lettersMatch = messageLettersPercentage >= letterPercentage return wordsMatch and lettersMatch def loadDictionary(): dictionaryFile = open('english.txt') # получаем объект файла словаря englishWords = {} # создается переменная englishWords в виде пустого словаря for word in dictionaryFile.read().split('\n'): englishWords[word] = None dictionaryFile.close() return englishWords #Строковый метод split() разбивает переданную ему строку по переносам, #возвращая список из нескольких строк ENGLISH_WORDS = loadDictionary() # вызывается функция loadDictionary() и возвращаемый ею словарь сохраняется # в переменной ENGLISH_WORDS. def getEnglishCount(message): message = message.upper() #символы строки преобразуются в верхний регистр message = remove_punctuation(message) #с помощью функции removeNonLetters() # из строки удаляются все небуквенные символы, такие как числа и знаки #препинания possibleWords = message.split() #метод split() разбивает строку на слова #и сохраняет их в переменной possibleWords if possibleWords == []: return 0.0 # слова отсутствуют, поэтому возвращаем 0.0. matches = 0 #для счетчика mаtсhеs устанавливается нулевое значение for word in possibleWords: #В цикле for мы проходим по всем словам в списке #possibleWords и проверяем существование каждого из них в словаре ENGLISH_WORDS if word in ENGLISH_WORDS: matches += 1 #Если слово содержится в словаре, значение счетчика #mаtсhеs инкрементируется return float(matches)/len(possibleWords) def main(): with open('Carroll.txt','r',encoding = 'utf-8') as file: plaintext = file.read() keyword = 'german' cl = ColTrans(keyword) ciphertext = cl.encipher(plaintext) print("\nВзлом шифра...\nНажмите Ctrl+C для завершения программы") englishPercentageOld = 0 for j in range(2,10): key1 = [i for i in range(j)] for p in permutations(key1): plaintext = ColTrans(p).decipher(ciphertext) englishPercentage = round(getEnglishCount(plaintext) * 100, 2) if englishPercentage > englishPercentageOld: print('Процент английских слов: %5s%%' % englishPercentage) print('Возможная длина ключа:',len(p)) print('Возможный ключ:',p) print('Возможный результат дешифровки:\n' + plaintext[:100]) englishPercentageOld = englishPercentage print('\nВзлом закончен') # Функция main() вызывается только в том случае, если файл ColTrans.py был # запущен как программа, а не импортируется в виде модуля другой программой if __name__ == '__main__': main()
Тут немного есть по теме RSA : https://book.jorianwoltjer.com/cryptography/asymmetric-encryption/rsa https://www.di-mgt.com.au/rsa_alg.html#rsasummary
Взлом шифра «Квадрат Полибия»Шифр имеет большое пространство ключей. При использовании квадрата размером [math]5\times 5[/math] количество ключей равно [math]25!=15511210043330985984\cdot 10^{6}=2^{83}[/math]. Единственное отличие шифра «Квадрат Полибия» от шифра простой замены заключается в том, что буква исходного текста замещается двумя символами. Для атаки используют методику, применяемую при взломе шифра простой замены — поиск восхождением к вершине. В качестве основного ключа выбирается случайный квадрат размером [math]5\times 5[/math]. В ходе каждой итерации ключ подвергается незначительным изменениям и проверяется насколько распределение триграмм или квадрограмм в тексте, полученном в результате расшифровки, соответствует распределению в естественном языке. Описание поиска восхождением к вершине на wikipedia
who_know777, написано по мотивам python-hillcipher/break.py at master · anag004/python-hillcipher · GitHub Код (Python): import itertools from math import log10 from copy import deepcopy LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' common_ngraphs = [] key = [[22, 9],[3, 6]] key_length = len(key) freqs = [] class ngram_score(object): def __init__(self,ngramfile): #загрузить файл содержащий n-граммы и счетчики, #вычислить логарифмические вероятности self.ngrams = {} filebuffer = open(ngramfile,encoding="UTF-8") for line in filebuffer: #n-граммы и счетчики разделены пробелом на одной строке key,count = line.split(' ') self.ngrams[key] = int(count) self.L = len(key) self.N = sum(self.ngrams.values()) #вычислить логарифмические вероятности for key in self.ngrams.keys(): self.ngrams[key] = log10(float(self.ngrams[key])/self.N) self.floor = log10(0.01/self.N) def score(self,text):# вычислить оценку текста score = 0 ngrams = self.ngrams.__getitem__ for i in range(len(text)-self.L+1): if text[i:i+self.L] in self.ngrams: score += ngrams(text[i:i+self.L]) else: score += self.floor return score class IncorrectLength(Exception): pass class InverseError(Exception): pass # Преобразовать символ в число mod len(LETTERS) def char2num(c): return LETTERS.find(c.upper()) # Преобразовать число mod len(LETTERS) в символ def num2char(n): return LETTERS[n].lower() #вычислить коэффициент Фридмана def friedman(text): n = 0 freq = [0 for i in range(len(LETTERS))] for c in text: freq[char2num(c)] += 1 n += 1 ic = 0 for f in freq: ic += f * (f - 1) ic = float(ic) / float((n * (n - 1))) return ic def get_mapping(c1, c2): s = "" for i in range(len(c1)): s += common_ngraphs[key_length - 2][c1[i]] + "->" + freqs[c2[i]][1] + " " return s def matrix_mult(mat, vec, m): n = len(mat) assert(len(vec) == n) # Create the result vector res = [0 for i in range(n)] for i in range(n): # Create a row sum variable row_sum = 0 for j in range(n): row_sum += mat[i][j] * vec[j] row_sum %= m res[i] = row_sum return res def get_key(d, e): n = len(d) mat1 = construct_matrix(d) mat2 = construct_matrix(e) mat1_inv = [] try: mat1_inv = matrix_inverse(mat1, len(LETTERS)) except InverseError: return (False, False, "diagraph non-invertible") key = matrix_mult2(mat2, mat1_inv, len(LETTERS)) key_inv = [] try: key_inv = matrix_inverse(deepcopy(key), len(LETTERS)) except InverseError: return (False, False, "key non-invertible") return (key, key_inv, "invertible") def matrix_mult2(mat1, mat2, m): assert(len(mat1[0]) == len(mat2)) rows = len(mat1) cols = len(mat2[0]) mid = len(mat1[0]) result = [[0 for i in range(cols)] for j in range(rows)] for i in range(rows): for j in range(cols): tmp = 0 for k in range(mid): tmp += mat1[i][k] * mat2[k][j] tmp %= m result[i][j] = tmp return result # Построить матрицу N на N из n-грамм def construct_matrix(d): n = len(d) ans = [[0 for i in range(n)] for j in range(n)] for i in range(n): for j in range(n): ans[j][i] = char2num(d[i][j]) return ans def get_diff(c1, c2): ans = 0 for i in range(len(c1)): ans += abs(c1[i] - c2[i]) return ans def gcd(a, b): if (a == 0 and b == 0): raise Exception("Cannot find gcd of zero and zero") if (b == 0): return [a, 1, 0] else: arr = gcd(b, a % b) q = a//b #int(a / b) return [arr[0], arr[2], arr[1] - q * arr[2]] def nCr(n, r): ans = 1 for i in range(r): ans *= (n - i) return ans # Поменять местами две строки матрицы def swap_row(mat, i, j): for k in range(len(mat[i])): mat[i][k], mat[j][k] = mat[j][k], mat[i][k] # Умножить строки матрицы на некоторый коэффициент def mult_row(mat, row_idx, mult_factor, m): for i in range(len(mat)): mat[row_idx][i] *= mult_factor mat[row_idx][i] = get_mod(mat[row_idx][i], m) # Сложить factor * (row j) матрицы до строки i def add_row(mat, i, j, factor, m): for k in range(len(mat)): mat[i][k] += (factor * mat[j][k]) mat[i][k] = get_mod(mat[i][k], m) def get_mod(a, m): if (a >= 0): return a % m else: return m - ((-a) % m) def mod_inverse(a, m): arr = gcd(a, m) if (arr[0]) != 1: raise InverseError else: return get_mod(arr[1], m) def matrix_inverse(mat, m): n = len(mat) # Create a n x n identity matrix res = [[0 for i in range(n)] for i in range(n)] for i in range(n): res[i][i] = 1 # Iterate over the rows to find nz_idx for row_idx in range(n): # Find a non-zero row nz_idx = -1 for i in range(row_idx, n): if (gcd(mat[i][row_idx], m)[0] == 1): nz_idx = i break if (nz_idx == -1): raise InverseError # Поменяйте местами эту строку с другой. swap_row(mat, row_idx, nz_idx) swap_row(res, row_idx, nz_idx) # Умножьте эту строку на элемент, чтобы сделать элемент head равным единице. mult_factor = mod_inverse(mat[row_idx][row_idx], m) mult_row(mat, row_idx, mult_factor, m) mult_row(res, row_idx, mult_factor, m) # Очистите все, что сверху и снизу. for i in range(n): if (i != row_idx): # Очистить эту строку mult_factor = mat[i][row_idx] add_row(mat, i, row_idx, -mult_factor, m) add_row(res, i, row_idx, -mult_factor, m) return res # Применяет матрицу к тексту и возвращает новый текст. def apply(mat, text): mat_length = len(mat) # Создать пустой массив символов out_text out_text = [] # Повторить текст curr_idx = 0 while curr_idx < len(text): # Создайте строку для обработки с помощью крестиков curr_text = [char2num('z') for i in range(mat_length)] # Отслеживайте позиции, в которые следует вставлять символы в out_text. insert_positions = [] # Подсчитайте количество прочитанных символов из текста. read_ctr = 0 # Наращивать curr_text while(read_ctr < mat_length and curr_idx < len(text)): if (text[curr_idx].isalpha()): # Это символ curr_text[read_ctr] = char2num(text[curr_idx]) # Вставьте фиктивный символ в out_text out_text.append('z') insert_positions.append(curr_idx) # Increment both counters read_ctr += 1 curr_idx += 1 else: # Скопировать текстовый символ в out_text out_text.append(text[curr_idx]) curr_idx += 1 # Выполнить умножение матриц mod len(LETTERS) и получить зашифрованный текст mat_text = matrix_mult(mat, curr_text, len(LETTERS)) # Поместите текст в out_text for i in range(read_ctr): out_text[insert_positions[i]] = num2char(mat_text[i]) # Добавить остаточный out_text for i in range(read_ctr, mat_length): out_text.append(num2char(mat_text[i])) # Преобразовать out_text в строку out_text = ''.join(out_text) return out_text def main(): plaintext = "hello world revolution" print('Исходный текст:\n'+plaintext) plaintext = plaintext.replace(' ','z') # Прочитать ключ как матрицу key = [[22, 9],[3, 6]] ciphertext = apply(key, plaintext) print('Зашифрованный текст:\n'+ciphertext) fitness = ngram_score('english_quadgrams.txt') # load our quadgram statistics # Получить обратную часть ключа key_inverse = matrix_inverse(key, len(LETTERS)) # Создать пустой массив символов открытого текста #plaintext = [] plaintext = apply(key_inverse, ciphertext) plaintext = plaintext.replace('z',' ') print('Расшифрованный текст:\n'+plaintext) print('Взламываю...') temp = int((len(ciphertext) - len('plaintext'))/2)+1 print(' c1 c2 \u2502'+' '*9+'KEY'+' '*8+'\u2502'+' '*temp+'plaintext'+' '*(temp+1)+'\u2502fitness') # Create a ngraph frequency table idx = {} num_ngraphs = 0 filebuffer = open('english_bigrams.txt',encoding="UTF-8") d = [] for line in filebuffer: key,count = line.split(' ') d.append(key.lower()) common_ngraphs.append(d) trial_length = len(common_ngraphs[0]) scores = [] tol = 0.01 ideal_ic = 0.0686 # индекс совпадений Фридмана для английского 0.0686, для русского 0.0553, для немецкого 0.0762 diff = 100 # Create a diagraph array read_ctr = 0 while(read_ctr < len(ciphertext)): ngraph = [] while(len(ngraph) < key_length): if (read_ctr >= len(ciphertext)): raise IncorrectLength("Length of ciphertext is not multiple of key length") if (ciphertext[read_ctr].isalpha()): ngraph.append(ciphertext[read_ctr]) read_ctr += 1 # Process the diagraph ngraph = ''.join(ngraph) if (ngraph in idx): freqs[idx[ngraph]][0] += 1 else: idx[ngraph] = num_ngraphs freqs.append([1, ngraph]) num_ngraphs += 1 freqs.sort(reverse=True) num_possibles = 0 found_keys = [] final_length = min(trial_length, len(freqs)) ctr = 0 total_tries = nCr(final_length, key_length) ** 2 x_old = -999.99 # Try various combinations of diagraphs for c1 in itertools.combinations(range(final_length), key_length): for c2 in itertools.combinations(range(final_length), key_length): # Print the progress progress = (100 * ctr) / total_tries ctr += 1 if (get_diff(c1, c2) > diff): continue (key, key_inv, verdict) = get_key([common_ngraphs[key_length - 2][c1[i]] for i in range(key_length)], [freqs[c2[i]][1] for i in range(key_length)]) if (key != False): plaintext = apply(key_inv, ciphertext) x = fitness.score(plaintext.upper()) plaintext_ic = friedman(plaintext) if (abs(plaintext_ic - ideal_ic) <= tol and (key not in found_keys)): # This can be a correct value if x_old <= x: scores.extend([(x,key,key_inv)]) print(get_mapping(c1, c2)+'\u2502%20s\u2502 %s \u2502%.2f'%(key,plaintext,x)) x_old = x max_key = max(scores) print('fitness = '+str(max_key[0])+':') print('лучший кандит на ключ = '+str(max_key[1])+':') print('Расшифрованный с помощью этого ключа текст:') plaintext = apply(max_key[2], ciphertext) print(plaintext.replace('z',' ')) # Если файл HillCipher.py выполняется как программа, # а не импортируется как модуль, вызвать функцию main() if __name__ == '__main__': main() Но срабатывает не всегда. Правильное срабатывание, в основном, на длинных текстах. А вот "Hello world" не распознает. Плюс скоро начало учебного года, а я не успеваю, поэтому и спрашиваю...
Шифр Хилла с матрицей 2х2 вскрывается довольно быстро бутфорсом, для матрицы 2х2 количество ключей 294=707281. Но возникает проблема, как найти осмысленный текст? Если брать английский текст, то сообщений с индексом совпадений Фридмана от 0.0644 до 0.0645 получается довольно много. Если сообщение короткое, то коэффициент Фридмана 0,05 - 0,07. По биграммам, триграммам, квадрограммам тоже сложно найти осмысленный текст. При дешифровке используется невырожденная матрица. Невырожденная матрица не может иметь детерминант равный нулю. Для матрицы 2х2 [math]P=\begin{bmatrix} a & b \\ c & d \end{bmatrix}[/math] определитель равен [math]det(P)=a\cdot d-b\cdot c[/math], должно выполняться условие [math]a\cdot d\ne b\cdot c[/math] Может быть есть еще идеи?
Взлом шифра Хилла 2х2написано на базе программы, предоставленной who_know777 Отбрасываем вырожденные матрицы Фильтрация невырожденных матриц ― пропускаем ключи с детерминантом, не взаимно простым с длиной алфавита. Проверка правдоподобия первой биграммы ― используем список наиболее часто употребимых биграмм английского языка. Вычисление индекса совпадений ― для фильтрации текстов, похожих на английский. Сортировка результатов ― по близости IC к значению 0.065. Используем словарь английских слов и выбираем сообщения, в которых более 40% английских слов Очень хорошо работает как с длинными, так и с короткими сообщениями от 8 символов. Огромное спасибо who_know777 Код (Python): import numpy as np import math, detectEnglish LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' # Преобразование текста в числовой формат def char2num(text): return [LETTERS.find(char) for char in text] # Преобразование числового представления в текст def num2char(numbers): return ''.join(LETTERS[num] for num in numbers) # Вычисление обратного элемента modulo m def mod_inverse(a, m): for x in range(1, m): if (a * x) % m == 1: return x return None # Вычисление обратной матрицы 2x2 по модулю len(LETTERS) def matrix_inverse(matrix): det = (matrix[0,0]*matrix[1,1] - matrix[0,1]*matrix[1,0]) % len(LETTERS) # Проверка обратимости матрицы if math.gcd(det, len(LETTERS)) != 1: return None det_inv = mod_inverse(det, len(LETTERS)) if det_inv is None: return None # Вычисление обратной матрицы inv_matrix = np.array([[matrix[1, 1], -matrix[0, 1]], [-matrix[1, 0], matrix[0, 0]]], dtype=int) * det_inv return inv_matrix % len(LETTERS) # Расшифровка текста с помощью ключевой матрицы def decrypt_hill(ciphertext, key_matrix): n = len(ciphertext) plaintext = [] # Получение обратной матрицы inv_key = matrix_inverse(key_matrix) if inv_key is None: return None # Расшифровка по биграммам for i in range(0, n, 2): if i + 1 < n: block = np.array([ciphertext[i], ciphertext[i+1]]) decrypted_block = np.dot(inv_key, block) % len(LETTERS) plaintext.extend(decrypted_block) return plaintext # Вычисление индекса совпадений (IC) def index_of_coincidence(text): n = len(text) if n < 2: return 0 freq = [0] * len(LETTERS) for c in text: freq[LETTERS.find(c)] += 1 ic = 0 for i in range(len(LETTERS)): ic += freq[i]*(freq[i]-1)/(n*(n - 1)) return ic # Проверка, является ли биграмма правдоподобной def is_plausible_bigram(bigram,common_bigrams,rare_bigrams): bigram_str = num2char(bigram) return bigram_str in common_bigrams or bigram_str not in rare_bigrams # Основная функция взлома def break_hill_2x2(ciphertext, min_ic=0.053, max_ic=0.078): ciphertext_num = char2num(ciphertext) n = len(ciphertext_num) if n < 4: print("Слишком короткий шифртекст для анализа") return # Наиболее употребимые биграммы в английском языке filebuffer = open('english_bigrams.txt',encoding="UTF-8") common_bigrams = [] for line in filebuffer: key,count = line.split(' ') common_bigrams.append(key) # Маловероятные биграммы rare_bigrams = ['JQ', 'QJ', 'QX', 'XQ', 'ZQ', 'QZ', 'JX', 'XJ', 'JZ', 'ZJ', 'ZX', 'XZ'] first_bigram = ciphertext_num[:2] candidates = [] # Перебор всех возможных ключей 2x2 for a in range(len(LETTERS)): for b in range(len(LETTERS)): for c in range(len(LETTERS)): for d in range(len(LETTERS)): key_matrix = np.array([[a, b], [c, d]]) # Пропускаем вырожденные матрицы if math.gcd((a*d - b*c) % len(LETTERS), len(LETTERS)) != 1: continue # Расшифровываем первую биграмму decrypted_bigram = decrypt_hill(first_bigram, key_matrix) if decrypted_bigram is None or not is_plausible_bigram(decrypted_bigram,common_bigrams,rare_bigrams): continue # Расшифровываем весь текст full_decryption = decrypt_hill(ciphertext_num, key_matrix) if full_decryption is None: continue # Вычисляем IC decrypted_text = num2char(full_decryption) ic = index_of_coincidence(decrypted_text) decrypted_text = decrypted_text.replace('Z',' ') # Проверяем, попадает ли IC в ожидаемый диапазон if min_ic <= ic <= max_ic: englishPercentage = detectEnglish.getEnglishCount(decrypted_text) * 100 if englishPercentage > 40: candidates.append({ 'key': key_matrix, 'text': decrypted_text, 'ic': ic, 'words': englishPercentage }) # Сортируем кандидатов по близости IC к 0.065 candidates.sort(key=lambda x: abs(x['ic'] - 0.065)) # Выводим результаты print(f"Найдено кандидатов: {len(candidates)}") for i, candidate in enumerate(candidates): # Показываем результаты print(f"#{i+1:0>3}: {candidate['words']:.2f} {candidate['text']}") return candidates def main(): ciphertext = "VOMKBGQOXWGBWONNHYTMYLGHZW" print('Зашифрованный текст:\n'+ciphertext) print("Взлом шифра Хилла 2x2...") print("Придется подождать пару минут...") candidates = break_hill_2x2(ciphertext) if __name__ == "__main__": main()