├── .editorconfig ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md └── en ├── images ├── .keep ├── 7X5A0921-closeup.png ├── 7X5A0927.jpg ├── 7X5A0933.jpg ├── 7X5A0957.jpg ├── Enigma-settings-line.jpg ├── Enigma-settings-sheet.jpg ├── Enigma-wiring.gif ├── OctaPi-Brute-Force-Enigma-TILE.png ├── banner.png ├── decrypt-settings.png ├── encrypt-settings.png ├── encrypted-message.png ├── enigma-canonical-qjf.png ├── enigma-canonical-qjg.png ├── enigma-pyenigma-encoding.png ├── intercepted-message.png └── terminal.png ├── meta.yml ├── resources ├── .keep ├── bruteforce_octapi.py ├── enigma_bf_canonical.py ├── enigma_bf_efficient.py ├── enigma_bf_standalone.py └── enigma_test.py ├── solutions ├── bruteforce_octapi.py ├── enigma_bf_canonical.py ├── enigma_bf_efficient.py ├── enigma_bf_standalone.py └── enigma_test.py ├── step_1.md ├── step_2.md ├── step_3.md ├── step_4.md ├── step_5.md ├── step_6.md ├── step_7.md ├── step_8.md └── step_9.md /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore files generated by https://github.com/CodeClub/slash-learning-content-preview 2 | */preview-*.html 3 | 4 | ### Python ### 5 | env/ 6 | 7 | ### Emacs ### 8 | \#*\# 9 | 10 | ### MacOS ### 11 | .DS_STORE 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All contributions are assumed to be licensed under the same licence as the source, i.e. [CC BY-SA](http://creativecommons.org/licenses/by-sa/4.0/). This licence must remain in all derivatives of this work. 4 | 5 | ## Issues 6 | 7 | If you find a mistake, bug or other problem, please [open an issue](https://github.com/raspberrypilearning/octapi-brute-force-enigma/issues) in this repository. 8 | 9 | ## Pull Requests 10 | 11 | All edits to a resource should be made to the draft branch, or to a fork of the repo. These patches can then be submitted as a pull request. 12 | 13 | If you fix a mistake, bug or problem or have something to contribute, please create a pull request for each modification. Please consider grouping modifications sensibly, i.e. don't bundle typo fixes in the same pull request as code changes, instead file them separately. 14 | 15 | Please note that sometimes things are done for pedagogical reasons so changes which make sense from a software engineering perspective (reducing duplication or making use of more advanced programming language features) may not be suitable to maintain the intended educational value. 16 | 17 | ## Derivatives 18 | 19 | See [LICENSE.md](LICENSE.md) for content licence. The licence must remain in all derivatives of this work. 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Licence 2 | 3 | Unless otherwise specified, everything in this repository is covered by the following licence: 4 | 5 | [![Creative Commons License](http://i.creativecommons.org/l/by-sa/4.0/88x31.png)](http://creativecommons.org/licenses/by-sa/4.0/) 6 | 7 | ***OctaPi: Brute Force Enigma*** by the [Raspberry Pi Foundation](http://www.raspberrypi.org) is licenced under a [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/). 8 | 9 | **Code and scripts copyright** 10 | [Crown Copyright](https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/) 11 | License: [Apache 2](https://www.apache.org/licenses/LICENSE-2.0) 12 | 13 | Based on a work at https://github.com/raspberrypilearning/octapi-brute-force-enigma. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OctaPi: Brute Force Enigma 2 | 3 | ![OctaPi: Brute Force Enigma](/en/images/banner.png) 4 | 5 | Make and then break Enigma-encoded messages using your Raspberry Pi or [OctaPi](https://github.com/raspberrypilearning/build-an-octapi). 6 | 7 | Find the project online at [projects.raspberrypi.org/en/projects/octapi-brute-force-enigma](https://projects.raspberrypi.org/en/projects/octapi-brute-force-enigma) 8 | 9 | ## Contributing 10 | See [CONTRIBUTING.md](CONTRIBUTING.md) 11 | 12 | ## Licence 13 | See [LICENSE.md](LICENSE.md) 14 | -------------------------------------------------------------------------------- /en/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/.keep -------------------------------------------------------------------------------- /en/images/7X5A0921-closeup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/7X5A0921-closeup.png -------------------------------------------------------------------------------- /en/images/7X5A0927.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/7X5A0927.jpg -------------------------------------------------------------------------------- /en/images/7X5A0933.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/7X5A0933.jpg -------------------------------------------------------------------------------- /en/images/7X5A0957.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/7X5A0957.jpg -------------------------------------------------------------------------------- /en/images/Enigma-settings-line.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/Enigma-settings-line.jpg -------------------------------------------------------------------------------- /en/images/Enigma-settings-sheet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/Enigma-settings-sheet.jpg -------------------------------------------------------------------------------- /en/images/Enigma-wiring.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/Enigma-wiring.gif -------------------------------------------------------------------------------- /en/images/OctaPi-Brute-Force-Enigma-TILE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/OctaPi-Brute-Force-Enigma-TILE.png -------------------------------------------------------------------------------- /en/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/banner.png -------------------------------------------------------------------------------- /en/images/decrypt-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/decrypt-settings.png -------------------------------------------------------------------------------- /en/images/encrypt-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/encrypt-settings.png -------------------------------------------------------------------------------- /en/images/encrypted-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/encrypted-message.png -------------------------------------------------------------------------------- /en/images/enigma-canonical-qjf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/enigma-canonical-qjf.png -------------------------------------------------------------------------------- /en/images/enigma-canonical-qjg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/enigma-canonical-qjg.png -------------------------------------------------------------------------------- /en/images/enigma-pyenigma-encoding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/enigma-pyenigma-encoding.png -------------------------------------------------------------------------------- /en/images/intercepted-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/intercepted-message.png -------------------------------------------------------------------------------- /en/images/terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/images/terminal.png -------------------------------------------------------------------------------- /en/meta.yml: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'OctaPi: brute-force Enigma' 3 | hero_image: images/banner.png 4 | description: Find out how to launch a brute-force crypt attack on Enigma 5 | theme: red 6 | duration: 1 7 | listed: true 8 | ingredient: false 9 | copyedit: true 10 | curriculum: 4, design-0, programming-4, phys-comp-0, manufacture-0, community-0 11 | interests: cybersecurity-and-cryptography 12 | technologies: python 13 | site_areas: projects 14 | hardware: '' 15 | software: python 16 | version: 4 17 | last_tested: 2017-11-01 18 | steps: 19 | - title: Introduction 20 | - title: What you will need 21 | - title: What is Enigma, and how does it work? 22 | - title: Enigma in use during WWII 23 | completion: 24 | - engaged 25 | - title: Encrypt a message 26 | - title: Decrypt a message 27 | - title: Standalone crypt attack 28 | - title: Crypt attack using OctaPi 29 | completion: 30 | - internal 31 | - title: 'Challenge: find out more' 32 | completion: 33 | - external 34 | -------------------------------------------------------------------------------- /en/resources/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raspberrypilearning/octapi-brute-force-enigma/90604b63a40a77dee014fb199371364ff06c62fe/en/resources/.keep -------------------------------------------------------------------------------- /en/resources/bruteforce_octapi.py: -------------------------------------------------------------------------------- 1 | import dispy, socket 2 | 3 | ciphertext = input('Cipher text ') 4 | cribtext = input('Crib text ') 5 | ring_choice = input('Ring settings ') 6 | 7 | rotors = [ "I II III", "I II IV", "I II V", "I III II", 8 | "I III IV", "I III V", "I IV II", "I IV III", 9 | "I IV V", "I V II", "I V III", "I V IV", 10 | "II I III", "II I IV", "II I V", "II III I", 11 | "II III IV", "II III V", "II IV I", "II IV III", 12 | "II IV V", "II V I", "II V III", "II V IV", 13 | "III I II", "III I IV", "III I V", "III II I", 14 | "III II IV", "III II V", "III IV I", "III IV II", 15 | "III IV V", "IV I II", "IV I III", "IV I V", 16 | "IV II I", "IV II III", "IV I V", "IV II I", 17 | "IV II III", "IV II V", "IV III I", "IV III II", 18 | "IV III V", "IV V I", "IV V II", "IV V III", 19 | "V I II", "V I III", "V I IV", "V II I", 20 | "V II III", "V II IV", "V III I", "V III II", 21 | "V III IV", "V IV I", "V IV II", "V IV III" ] 22 | 23 | 24 | def find_rotor_start( rotor_choice, ciphertext, cribtext, ring_choice ): 25 | from enigma.machine import EnigmaMachine 26 | 27 | alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 28 | 29 | machine = EnigmaMachine.from_key_sheet( 30 | rotors=rotor_choice, 31 | reflector='B', 32 | ring_settings=ring_choice, 33 | plugboard_settings='AV BS CG DL FU HZ IN KM OW RX') 34 | 35 | # Do a search over all possible rotor starting positions 36 | for rotor1 in alphabet: # Search for rotor 1 start position 37 | for rotor2 in alphabet: # Search for rotor 2 start position 38 | for rotor3 in alphabet: # Search for rotor 3 start position 39 | 40 | # Generate a possible rotor start position 41 | start_pos = rotor1 + rotor2 + rotor3 42 | 43 | # Set the starting position 44 | machine.set_display(start_pos) 45 | 46 | # Attempt to decrypt the plaintext 47 | plaintext = machine.process_text(ciphertext) 48 | print( plaintext ) 49 | 50 | # Check if decrypted version is the same as the crib text 51 | if plaintext == cribtext: 52 | print("Valid settings found!") 53 | return rotor_choice, ring_choice, start_pos 54 | 55 | # If we didn't manage to successfully decrypt the message 56 | return rotor_choice, ring_choice, "Cannot find settings" 57 | 58 | 59 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 60 | s.connect(("8.8.8.8", 80)) # doesn't matter if 8.8.8.8 can't be reached 61 | cluster = dispy.JobCluster(find_rotor_start, ip_addr=s.getsockname()[0], nodes='192.168.1.*') 62 | 63 | jobs = [] 64 | id = 1 65 | 66 | for rotor_choice in rotors: 67 | job = cluster.submit( rotor_choice, ciphertext, cribtext, ring_choice ) 68 | job.id = id # Associate an ID to the job 69 | jobs.append(job) 70 | id += 1 # Next job 71 | print( "Waiting..." ) 72 | cluster.wait() 73 | print( "Collecting job results" ) 74 | 75 | found = False 76 | for job in jobs: 77 | # Wait for job to finish and return results 78 | rotor_setting, ring_setting, start_pos = job() 79 | 80 | # If a start position was found 81 | if start_pos != "Cannot find settings": 82 | found = True 83 | print( "Rotors %s, ring %s, message key was %s, using crib %s" % (rotor_setting, ring_setting, start_pos, cribtext) ) 84 | 85 | if found == False: 86 | print( 'Attack unsuccessful' ) 87 | 88 | cluster.print_status() 89 | cluster.close() 90 | -------------------------------------------------------------------------------- /en/resources/enigma_bf_canonical.py: -------------------------------------------------------------------------------- 1 | # This code attempts a partial brute force attack on Enigma messages. 2 | # Messages may be created on a real machine, compatible replica, the 3 | # Cryptoy Android App or any application that accurately reproduces 4 | # the 3-rotor machine used by the german armed forces. We have used 5 | # Brian Neal's Py-enigma Python3 library and utilities. 6 | # 7 | # This code uses Dispy on OctaPi using the recommended method for managing 8 | # jobs efficiently. For more information, visit the Dispy website. 9 | # 10 | 11 | # Dispy: 12 | # Giridhar Pemmasani, "dispy: Distributed and parallel Computing with/for Python", 13 | # http://dispy.sourceforge.net, 2016 14 | 15 | # Py-enigma: 16 | # Brian Neal 17 | # http://py-enigmareadthedocs.org/ 18 | # License: MIT License 19 | 20 | # All other original code: Crown Copyright 2016, 2017 21 | 22 | # Assumes message was encrypted as follows: 23 | # 24 | # STEP 1: Choose a start position and message key 25 | # where: UYT is initial rotor position (chosen 'randomly') 26 | # SCC is unencrypted message key (chosen 'randomly') 27 | # 28 | # STEP 2: Encrypyt the message key 29 | # pyenigma.py -r II V III -i 1 1 1 -p AV BS CG DL FU HZ IN KM OW RX -u B --start=UYT --text='SCC' 30 | # PWE 31 | # where: PWE is encrypted message key 32 | # 33 | # Note: We use a one-to-one relation for the rotor ring for compatability with Cryptoy 34 | # 35 | # STEP 3: Encrypt the message using the unencrypted message key 36 | # pyenigma.py --key-file=keys.txt --start='SCC' --text='THISXISXWORKING' 37 | # YJPYITREDSYUPIU 38 | # where: 39 | # SCC is the unencrypted message key 40 | # YJPYITREDSYUPIU is the cypher text produced 41 | # 42 | # STEP 4: Operator sends (usually in Morse): 43 | # STNA DE STNB 1104 = 15 = UYT PWE = BNUGZ YJPYI TREDS YUPIU 44 | # where: 45 | # STNA is callsign of destination station 46 | # DE means 'from' in Morse abreviation 47 | # STNB is callsign of originating station 48 | # = is the 'break' Morse character, which is used as a delimiter 49 | # 1104 is time message is sent (presumably UTC) 50 | # BNUGZ contains UGZ 'Kenngruppen' (day indicator for confirmation of correct key sheet entry on decrypt) 51 | # 52 | # 53 | 54 | ################################ 55 | # Brute Force attack 56 | # This is a limited brute force attack on the rotor settings assuming no plugboard and no rotor ring 57 | # We use 'THISXISXWORKING' as the crib message 58 | ################################ 59 | 60 | rotor = [ "I II III", "I II IV", "I II V", "I III II", 61 | "I III IV", "I III V", "I IV II", "I IV III", 62 | "I IV V", "I V II", "I V III", "I V IV", 63 | "II I III", "II I IV", "II I V", "II III I", 64 | "II III IV", "II III V", "II IV I", "II IV III", 65 | "II IV V", "II V I", "II V III", "II V IV", 66 | "III I II", "III I IV", "III I V", "III II I", 67 | "III II IV", "III II V", "III IV I", "III IV II", 68 | "III IV V", "IV I II", "IV I III", "IV I V", 69 | "IV II I", "IV II III", "IV I V", "IV II I", 70 | "IV II III", "IV II V", "IV III I", "IV III II", 71 | "IV III V", "IV V I", "IV V II", "IV V III", 72 | "V I II", "V I III", "V I IV", "V II I", 73 | "V II III", "V II IV", "V III I", "V III II", 74 | "V III IV", "V IV I", "V IV II", "V IV III" ] 75 | 76 | ring = [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", 77 | "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", 78 | "21", "22", "23", "24", "25", "26" ] 79 | 80 | # 81 | # This function does an exhaust search over the list of possible 82 | # rotor selections 83 | # 84 | def find_rotor_start( rotor_choice, ring_choice, ciphertext, cribtext ): 85 | 86 | from enigma.machine import EnigmaMachine 87 | 88 | alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 89 | 90 | machine = EnigmaMachine.from_key_sheet( 91 | rotors=rotor_choice, 92 | reflector='B', 93 | ring_settings=ring_choice, 94 | plugboard_settings='AV BS CG DL FU HZ IN KM OW RX') # plugboard known 95 | 96 | 97 | # do an exhaust search over all possible rotor starting positions 98 | for i in range(len(alphabet)): # search for rotor 1 start position 99 | for j in range(len(alphabet)): # search for rotor 2 start position 100 | for k in range(len(alphabet)): # search for rotor 3 start position 101 | # generate a possible rotor start position 102 | start_pos = alphabet[i] + alphabet[j] + alphabet[k] 103 | 104 | # set machine initial starting position and attempt decrypt 105 | machine.set_display(start_pos) 106 | plaintext = machine.process_text(ciphertext) 107 | 108 | # check if decrypt is the same as the crib text 109 | if (plaintext == cribtext): 110 | # print( start_pos, plaintext, cribtext ) 111 | return( rotor_choice, ring_choice, start_pos ) 112 | 113 | return( rotor_choice, ring_choice, "null" ) 114 | 115 | 116 | # main loop 117 | if __name__ == '__main__': 118 | import argparse, dispy, resource 119 | 120 | resource.setrlimit(resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY) ) 121 | resource.setrlimit(resource.RLIMIT_DATA, (resource.RLIM_INFINITY, resource.RLIM_INFINITY) ) 122 | 123 | parser = argparse.ArgumentParser() 124 | parser.add_argument("ciphertext", help="cipher text, which is the encrypted text to be broken") 125 | parser.add_argument("cribtext", help="crib text, which is known message content") 126 | parser.add_argument("ring_choice", help="slip ring setting to use, eg '1 1 1'") 127 | args = parser.parse_args() 128 | 129 | # extract the cipher and crib texts from the command line 130 | ciphertext = args.ciphertext 131 | cribtext = args.cribtext 132 | ring_choice = args.ring_choice 133 | 134 | cluster = dispy.JobCluster(find_rotor_start, nodes='192.168.1.*') 135 | jobs = [] 136 | id = 1 # job id 137 | 138 | print(( "Brute force crypt attack on Enigma message %s using crib %s" % (ciphertext, cribtext) )) 139 | 140 | # try all rotor settings (choosing three from five) 141 | print( 'Trying all rotor setings for ring choice "%s" ...' % (ring_choice) ) 142 | 143 | # submit the jobs for this ring choice 144 | for rotor_choice in rotor: 145 | job = cluster.submit( rotor_choice, ring_choice, ciphertext, cribtext ) 146 | job.id = id # associate an ID to the job 147 | jobs.append(job) 148 | id += 1 # next job 149 | 150 | 151 | print( "Waiting..." ) 152 | cluster.wait() 153 | print( "Collecting job results" ) 154 | 155 | # collect and check through the jobs for this ring setting 156 | found = False 157 | for job in jobs: 158 | rotor_setting, ring_setting, start_pos = job() # waits for job to finish and returns results 159 | if (start_pos != "null"): 160 | found = True 161 | print(( "Machine setting found: rotors %s, ring %s, message key was %s, using crib %s" % (rotor_setting, ring_setting, start_pos, cribtext) )) 162 | 163 | if (found == False): print( 'Attack unsuccessfull' ) 164 | 165 | cluster.print_status() 166 | cluster.close() 167 | -------------------------------------------------------------------------------- /en/resources/enigma_bf_efficient.py: -------------------------------------------------------------------------------- 1 | # This code attempts a partial brute force attack on Enigma messages. 2 | # Messages may be created on a real machine, compatible replica, the 3 | # Cryptoy Android App or any application that accurately reproduces 4 | # the 3-rotor machine used by the german armed forces. We have used 5 | # Brian Neal's Py-enigma Python3 library and utilities. 6 | # 7 | # This code uses Dispy on OctaPi using the recommended method for managing 8 | # jobs efficiently. For more information, visit the Dispy website. 9 | # 10 | 11 | # Dispy: 12 | # Giridhar Pemmasani, "dispy: Distributed and parallel Computing with/for Python", 13 | # http://dispy.sourceforge.net, 2016 14 | 15 | # Py-enigma: 16 | # Brian Neal 17 | # http://py-enigmareadthedocs.org/ 18 | # License: MIT License 19 | 20 | # All other original code: Crown Copyright 2016, 2017 21 | 22 | 23 | # Assumes message was encrypted as follows: 24 | # 25 | # STEP 1: Choose a start position and message key 26 | # where: UYT is initial rotor position (chosen 'randomly') 27 | # SCC is unencrypted message key (chosen 'randomly') 28 | # 29 | # STEP 2: Encrypyt the message key 30 | # pyenigma.py -r II V III -i 1 1 1 -p AV BS CG DL FU HZ IN KM OW RX -u B --start=UYT --text='SCC' 31 | # PWE 32 | # where: PWE is encrypted message key 33 | # 34 | # Note: We use a one-to-one relation for the rotor ring for compatability with Cryptoy 35 | # 36 | # STEP 3: Encrypt the message using the unencrypted message key 37 | # pyenigma.py --key-file=keys.txt --start='SCC' --text='THISXISXWORKING' 38 | # YJPYITREDSYUPIU 39 | # where: 40 | # SCC is the unencrypted message key 41 | # YJPYITREDSYUPIU is the cypher text produced 42 | # 43 | # STEP 4: Operator sends (usually in Morse): 44 | # STNA DE STNB 1104 = 15 = UYT PWE = BNUGZ YJPYI TREDS YUPIU 45 | # where: 46 | # STNA is callsign of destination station 47 | # DE means 'from' in Morse abreviation 48 | # STNB is callsign of originating station 49 | # = is the 'break' Morse character, which is used as a delimiter 50 | # 1104 is time message is sent (presumably UTC) 51 | # BNUGZ contains UGZ 'Kenngruppen' (day indicator for confirmation of correct key sheet entry on decrypt) 52 | # 53 | # 54 | 55 | ################################ 56 | # Brute Force attack 57 | # This is a limited brue force attack on the rotor settings assuming no plugboard and no rotor ring 58 | # We use 'THISXISXWORKING' as the crib message 59 | ################################ 60 | 61 | # setup machine to be the same as that used for encrypt 62 | 63 | character = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 64 | rotor = [ "I II III", "I II IV", "I II V", "I III II", 65 | "I III IV", "I III V", "I IV II", "I IV III", 66 | "I IV V", "I V II", "I V III", "I V IV", 67 | "II I III", "II I IV", "II I V", "II III I", 68 | "II III IV", "II III V", "II IV I", "II IV III", 69 | "II IV V", "II V I", "II V III", "II V IV", 70 | "III I II", "III I IV", "III I V", "III II I", 71 | "III II IV", "III II V", "III IV I", "III IV II", 72 | "III IV V", "IV I II", "IV I III", "IV I V", 73 | "IV II I", "IV II III", "IV I V", "IV II I", 74 | "IV II III", "IV II V", "IV III I", "IV III II", 75 | "IV III V", "IV V I", "IV V II", "IV V III", 76 | "V I II", "V I III", "V I IV", "V II I", 77 | "V II III", "V II IV", "V III I", "V III II", 78 | "V III IV", "V IV I", "V IV II", "V IV III" ] 79 | 80 | ring = [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", 81 | "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", 82 | "21", "22", "23", "24", "25", "26" ] 83 | 84 | # 85 | # This function does an exhaust search over the list of possible 86 | # rotor selections 87 | # 88 | def find_rotor_start( rotor_choice, ring_choice, ciphertext, cribtext ): 89 | 90 | from enigma.machine import EnigmaMachine 91 | 92 | alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 93 | 94 | machine = EnigmaMachine.from_key_sheet( 95 | rotors=rotor_choice, 96 | reflector='B', 97 | ring_settings=ring_choice, 98 | plugboard_settings='AV BS CG DL FU HZ IN KM OW RX') # plugboard known 99 | 100 | 101 | # do an exhaust search over all possible rotor starting positions 102 | for i in range(len(alphabet)): # search for rotor 1 start position 103 | for j in range(len(alphabet)): # search for rotor 2 start position 104 | for k in range(len(alphabet)): # search for rotor 3 start position 105 | # generate a possible rotor start position 106 | start_pos = alphabet[i] + alphabet[j] + alphabet[k] 107 | 108 | # set machine initial starting position and attempt decrypt 109 | machine.set_display(start_pos) 110 | plaintext = machine.process_text(ciphertext) 111 | 112 | # check if decrypt is the same as the crib text 113 | if (plaintext == cribtext): 114 | # print( start_pos, plaintext, cribtext ) 115 | return( rotor_choice, ring_choice, start_pos ) 116 | 117 | return( rotor_choice, ring_choice, "null" ) 118 | 119 | 120 | 121 | # dispy calls this function to indicate change in job status 122 | def job_callback(job): # executed at the client 123 | global pending_jobs, jobs_cond 124 | global found 125 | 126 | if (job.status == dispy.DispyJob.Finished # most usual case 127 | or job.status in (dispy.DispyJob.Terminated, dispy.DispyJob.Cancelled, 128 | dispy.DispyJob.Abandoned)): 129 | # 'pending_jobs' is shared between two threads, so access it with 130 | # 'jobs_cond' (see below) 131 | jobs_cond.acquire() 132 | if job.id: # job may have finished before 'main' assigned id 133 | pending_jobs.pop(job.id) 134 | 135 | # extract the results for each job as it happens 136 | rotor_choice, ring_choice, start_pos = job.result # returns results from job 137 | if (start_pos != "null"): 138 | found = True 139 | dispy.logger.info( 'Machine setting found: job "%i" returned "%s" with ring "%s" using "%s", %s jobs pending', job.id, rotor_choice, ring_choice, start_pos, len(pending_jobs) ) 140 | 141 | if len(pending_jobs) <= lower_bound: 142 | jobs_cond.notify() 143 | 144 | jobs_cond.release() 145 | 146 | 147 | # main loop 148 | if __name__ == '__main__': 149 | import dispy, argparse, resource, threading, logging 150 | 151 | # set lower and upper bounds as appropriate 152 | # lower_bound is at least num of cpus and upper_bound is roughly 3x lower_bound 153 | # lower_bound, upper_bound = 352, 1056 154 | lower_bound, upper_bound = 32, 96 155 | 156 | resource.setrlimit(resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY) ) 157 | resource.setrlimit(resource.RLIMIT_DATA, (resource.RLIM_INFINITY, resource.RLIM_INFINITY) ) 158 | 159 | parser = argparse.ArgumentParser() 160 | parser.add_argument("ciphertext", help="cipher text, which is the encrypted text to be broken") 161 | parser.add_argument("cribtext", help="crib text, which is known message content") 162 | args = parser.parse_args() 163 | 164 | # extract the cipher and crib texts from the command line 165 | ciphertext = args.ciphertext 166 | cribtext = args.cribtext 167 | 168 | server_nodes ='192.168.1.*' 169 | 170 | # use Condition variable to protect access to pending_jobs, as 171 | # 'job_callback' is executed in another thread 172 | jobs_cond = threading.Condition() 173 | 174 | pending_jobs = {} 175 | cluster = dispy.JobCluster(find_rotor_start, nodes=server_nodes, callback=job_callback, loglevel=logging.INFO) 176 | 177 | print( "Brute force crypt attack on Enigma message %s using crib %s" % (ciphertext, cribtext) ) 178 | 179 | # try all rotor settings (choosing three from five) 180 | found = False 181 | i = 1 # job counter 182 | r0 = 0 # ring 0 counter 183 | r1 = 0 # ring 1 counter 184 | r2 = 0 # ring 2 counter 185 | while (r0 < len(ring)) and (found == False): 186 | while (r1 < len(ring)) and (found == False): 187 | while (r2 < len(ring)) and (found == False): 188 | 189 | ring_choice = ring[r0] + " " + ring[r1] + " " + ring[r2] 190 | rot = 1 # rotor combination counter 191 | 192 | while (rot < len(rotor)) and (found == False): 193 | rotor_choice = rotor[rot] 194 | 195 | print( 'Trying rotor "%s" with ring "%s" ...' % (rotor_choice, ring_choice) ) 196 | 197 | # schedule execution of find_rotor_start (running 'dispynode') 198 | job = cluster.submit( rotor_choice, ring_choice, ciphertext, cribtext ) 199 | 200 | jobs_cond.acquire() 201 | 202 | job.id = i # associate an ID to the job 203 | 204 | # there is a chance the job may have finished and job_callback called by 205 | # this time, so put it in 'pending_jobs' only if job is pending 206 | if job.status == dispy.DispyJob.Created or job.status == dispy.DispyJob.Running: 207 | pending_jobs[i] = job 208 | # dispy.logger.info('job "%s" submitted: %s', i, len(pending_jobs)) 209 | if len(pending_jobs) >= upper_bound: 210 | while len(pending_jobs) > lower_bound: 211 | jobs_cond.wait() 212 | jobs_cond.release() 213 | 214 | rot += 1 # next rotor combination 215 | 216 | i += 1 # next job 217 | r2 += 1 # next ring 2 setting 218 | r1 += 1 # next ring 1 setting 219 | r0 += 1 # next ring 0 setting 220 | 221 | cluster.wait() 222 | 223 | if (found == False): print( 'Attack unsuccessfull' ) 224 | 225 | cluster.print_status() 226 | cluster.close() 227 | -------------------------------------------------------------------------------- /en/resources/enigma_bf_standalone.py: -------------------------------------------------------------------------------- 1 | # This code attempts a partial brute force attack on Enigma messages. 2 | # Messages may be created on a real machine, compatible replica, the 3 | # Cryptoy Android App or any application that accurately reproduces 4 | # the 3-rotor machine used by the german armed forces. We have used 5 | # Brian Neal's Py-enigma Python3 library and utilities. 6 | # 7 | # This code runs standalone on the client and allows you to compare 8 | # compute runtime with the Dispy version running on OctaPi. 9 | # 10 | 11 | # Dispy: 12 | # Giridhar Pemmasani, "dispy: Distributed and parallel Computing with/for Python", 13 | # http://dispy.sourceforge.net, 2016 14 | 15 | # Py-enigma: 16 | # Brian Neal 17 | # http://py-enigmareadthedocs.org/ 18 | # License: MIT License 19 | 20 | # All other original code: Crown Copyright 2016, 2017 21 | 22 | # Assumes message was encrypted as follows: 23 | # 24 | # STEP 1: Choose a start position and message key 25 | # where: UYT is initial rotor position (chosen 'randomly') 26 | # SCC is unencrypted message key (chosen 'randomly') 27 | # 28 | # STEP 2: Encrypyt the message key 29 | # pyenigma.py -r II V III -i 1 1 1 -p AV BS CG DL FU HZ IN KM OW RX -u B --start=UYT --text='SCC' 30 | # PWE 31 | # where: PWE is encrypted message key 32 | # 33 | # Note: We use a one-to-one relation for the rotor ring for compatability with Cryptoy 34 | # 35 | # STEP 3: Encrypt the message using the unencrypted message key 36 | # pyenigma.py --key-file=keys.txt --start='SCC' --text='THISXISXWORKING' 37 | # YJPYITREDSYUPIU 38 | # where: 39 | # SCC is the unencrypted message key 40 | # YJPYITREDSYUPIU is the cypher text produced 41 | # 42 | # STEP 4: Operator sends (usually in Morse): 43 | # STNA DE STNB 1104 = 15 = UYT PWE = BNUGZ YJPYI TREDS YUPIU 44 | # where: 45 | # STNA is callsign of destination station 46 | # DE means 'from' in Morse abreviation 47 | # STNB is callsign of originating station 48 | # = is the 'break' Morse character, which is used as a delimiter 49 | # 1104 is time message is sent (presumably UTC) 50 | # BNUGZ contains UGZ 'Kenngruppen' (day indicator for confirmation of correct key sheet entry on decrypt) 51 | # 52 | # 53 | 54 | ################################ 55 | # Brute Force attack 56 | # This is a limited brue force attack on the rotor settings assuming no plugboard and no rotor ring 57 | # We use 'THISXISXWORKING' as the crib message 58 | ################################ 59 | 60 | rotor = [ "I II III", "I II IV", "I II V", "I III II", 61 | "I III IV", "I III V", "I IV II", "I IV III", 62 | "I IV V", "I V II", "I V III", "I V IV", 63 | "II I III", "II I IV", "II I V", "II III I", 64 | "II III IV", "II III V", "II IV I", "II IV III", 65 | "II IV V", "II V I", "II V III", "II V IV", 66 | "III I II", "III I IV", "III I V", "III II I", 67 | "III II IV", "III II V", "III IV I", "III IV II", 68 | "III IV V", "IV I II", "IV I III", "IV I V", 69 | "IV II I", "IV II III", "IV I V", "IV II I", 70 | "IV II III", "IV II V", "IV III I", "IV III II", 71 | "IV III V", "IV V I", "IV V II", "IV V III", 72 | "V I II", "V I III", "V I IV", "V II I", 73 | "V II III", "V II IV", "V III I", "V III II", 74 | "V III IV", "V IV I", "V IV II", "V IV III" ] 75 | 76 | # 77 | # This function does an exhaust search over the list of possible 78 | # rotor selections 79 | # 80 | def find_rotor_start( rotor_choice, ciphertext, cribtext ): 81 | 82 | from enigma.machine import EnigmaMachine 83 | 84 | alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 85 | 86 | machine = EnigmaMachine.from_key_sheet( 87 | rotors=rotor_choice, 88 | reflector='B', 89 | ring_settings='1 1 1', # no ring setting 90 | plugboard_settings='AV BS CG DL FU HZ IN KM OW RX') # plugboard known 91 | 92 | 93 | # do an exhaust search over all possible rotor starting positions 94 | for i in range(len(alphabet)): # search for rotor 1 start position 95 | for j in range(len(alphabet)): # search for rotor 2 start position 96 | for k in range(len(alphabet)): # search for rotor 3 start position 97 | # generate a possible rotor start position 98 | start_pos = alphabet[i] + alphabet[j] + alphabet[k] 99 | 100 | # set machine initial starting position and attempt decrypt 101 | machine.set_display(start_pos) 102 | plaintext = machine.process_text(ciphertext) 103 | 104 | # check if decrypt is the same as the crib text 105 | if (plaintext == cribtext): 106 | # print( start_pos, plaintext, cribtext ) 107 | return( rotor_choice, start_pos ) 108 | 109 | return( rotor_choice, "null" ) 110 | 111 | 112 | # main loop 113 | if __name__ == '__main__': 114 | import argparse 115 | 116 | parser = argparse.ArgumentParser() 117 | parser.add_argument("ciphertext", help="cipher text, which is the encrypted text to be broken") 118 | parser.add_argument("cribtext", help="crib text, which is known message content") 119 | args = parser.parse_args() 120 | 121 | # extract the cipher and crib texts from the command line 122 | ciphertext = args.ciphertext 123 | cribtext = args.cribtext 124 | 125 | print(( "Brute force crypt attack on Enigma message %s using crib %s" % (ciphertext, cribtext) )) 126 | 127 | # try all rotor settings (choosing three from five) 128 | for rotor_setting in rotor: 129 | print(( "Trying rotors %s..." % (rotor_setting) )) 130 | rotor_choice, start_pos = find_rotor_start( rotor_setting, ciphertext, cribtext ) 131 | if (start_pos != "null"): 132 | print(( "Machine setting found: rotors %s, message key was %s, using crib %s" % (rotor_choice, start_pos, cribtext) )) 133 | exit(0) 134 | -------------------------------------------------------------------------------- /en/resources/enigma_test.py: -------------------------------------------------------------------------------- 1 | # This code is the original example provided with the Py-enigma 2 | # Python3 library and utilities. It is included here to confirm 3 | # correct operation of Py-enigma on your system before proceeding 4 | # with the brute force attack code. 5 | # 6 | 7 | # Py-enigma: 8 | # Brian Neal 9 | # http://py-enigmareadthedocs.org/ 10 | # License: MIT License 11 | 12 | # All other original code: Crown Copyright 2016, 2017 13 | 14 | # Assumes message was encrypted as follows: 15 | # 16 | # STEP 1: Choose a start position and message key 17 | # where: UYT is initial rotor position (chosen 'randomly') 18 | # SCC is unencrypted message key (chosen 'randomly') 19 | # 20 | # STEP 2: Encrypyt the message key 21 | # pyenigma.py -r II V III -i 1 1 1 -p AV BS CG DL FU HZ IN KM OW RX -u B --start=UYT --text='SCC' 22 | # PWE 23 | # where: PWE is encrypted message key 24 | # 25 | # Note: We use a one-to-one relation for the rotor ring for compatability with Cryptoy 26 | # 27 | # STEP 3: Encrypt the message using the unencrypted message key 28 | # pyenigma.py --key-file=keys.txt --start='SCC' --text='THISXISXWORKING' 29 | # YJPYITREDSYUPIU 30 | # where: 31 | # SCC is the unencrypted message key 32 | # YJPYITREDSYUPIU is the cypher text produced 33 | # 34 | # STEP 4: Operator sends (usually in Morse): 35 | # STNA DE STNB 1104 = 15 = UYT PWE = BNUGZ YJPYI TREDS YUPIU 36 | # where: 37 | # STNA is callsign of destination station 38 | # DE means 'from' in Morse abreviation 39 | # STNB is callsign of originating station 40 | # = is the 'break' Morse character, which is used as a delimiter 41 | # 1104 is time message is sent (presumably UTC) 42 | # BNUGZ contains UGZ 'Kenngruppen' (day indicator for confirmation of correct key sheet entry on decrypt) 43 | # 44 | # 45 | 46 | from enigma.machine import EnigmaMachine 47 | 48 | ################################ 49 | # Decrypt 50 | ################################ 51 | 52 | # setup machine to be the same as that used for encrypt 53 | 54 | machine = EnigmaMachine.from_key_sheet( 55 | rotors='II V III', 56 | reflector='B', 57 | ring_settings='1 1 1', 58 | plugboard_settings='AV BS CG DL FU HZ IN KM OW RX') 59 | 60 | # set machine initial starting position 61 | machine.set_display('UYT') 62 | 63 | # decrypt the message key 64 | msg_key = machine.process_text('PWE') 65 | 66 | # decrypt the cipher text with the unencrypted message key 67 | machine.set_display(msg_key) 68 | 69 | ciphertext = 'YJPYITREDSYUPIU' 70 | plaintext = machine.process_text(ciphertext) 71 | 72 | print(plaintext) 73 | -------------------------------------------------------------------------------- /en/solutions/bruteforce_octapi.py: -------------------------------------------------------------------------------- 1 | import dispy, socket 2 | 3 | ciphertext = input('Cipher text ') 4 | cribtext = input('Crib text ') 5 | ring_choice = input('Ring settings ') 6 | 7 | rotors = [ "I II III", "I II IV", "I II V", "I III II", 8 | "I III IV", "I III V", "I IV II", "I IV III", 9 | "I IV V", "I V II", "I V III", "I V IV", 10 | "II I III", "II I IV", "II I V", "II III I", 11 | "II III IV", "II III V", "II IV I", "II IV III", 12 | "II IV V", "II V I", "II V III", "II V IV", 13 | "III I II", "III I IV", "III I V", "III II I", 14 | "III II IV", "III II V", "III IV I", "III IV II", 15 | "III IV V", "IV I II", "IV I III", "IV I V", 16 | "IV II I", "IV II III", "IV I V", "IV II I", 17 | "IV II III", "IV II V", "IV III I", "IV III II", 18 | "IV III V", "IV V I", "IV V II", "IV V III", 19 | "V I II", "V I III", "V I IV", "V II I", 20 | "V II III", "V II IV", "V III I", "V III II", 21 | "V III IV", "V IV I", "V IV II", "V IV III" ] 22 | 23 | 24 | def find_rotor_start( rotor_choice, ciphertext, cribtext, ring_choice ): 25 | from enigma.machine import EnigmaMachine 26 | 27 | alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 28 | 29 | machine = EnigmaMachine.from_key_sheet( 30 | rotors=rotor_choice, 31 | reflector='B', 32 | ring_settings=ring_choice, 33 | plugboard_settings='AV BS CG DL FU HZ IN KM OW RX') 34 | 35 | # Do a search over all possible rotor starting positions 36 | for rotor1 in alphabet: # Search for rotor 1 start position 37 | for rotor2 in alphabet: # Search for rotor 2 start position 38 | for rotor3 in alphabet: # Search for rotor 3 start position 39 | 40 | # Generate a possible rotor start position 41 | start_pos = rotor1 + rotor2 + rotor3 42 | 43 | # Set the starting position 44 | machine.set_display(start_pos) 45 | 46 | # Attempt to decrypt the plaintext 47 | plaintext = machine.process_text(ciphertext) 48 | print( plaintext ) 49 | 50 | # Check if decrypted version is the same as the crib text 51 | if plaintext == cribtext: 52 | print("Valid settings found!") 53 | return rotor_choice, ring_choice, start_pos 54 | 55 | # If we didn't manage to successfully decrypt the message 56 | return rotor_choice, ring_choice, "Cannot find settings" 57 | 58 | 59 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 60 | s.connect(("8.8.8.8", 80)) # doesn't matter if 8.8.8.8 can't be reached 61 | cluster = dispy.JobCluster(find_rotor_start, ip_addr=s.getsockname()[0], nodes='192.168.1.*') 62 | 63 | jobs = [] 64 | id = 1 65 | 66 | for rotor_choice in rotors: 67 | job = cluster.submit( rotor_choice, ciphertext, cribtext, ring_choice ) 68 | job.id = id # Associate an ID to the job 69 | jobs.append(job) 70 | id += 1 # Next job 71 | print( "Waiting..." ) 72 | cluster.wait() 73 | print( "Collecting job results" ) 74 | 75 | found = False 76 | for job in jobs: 77 | # Wait for job to finish and return results 78 | rotor_setting, ring_setting, start_pos = job() 79 | 80 | # If a start position was found 81 | if start_pos != "Cannot find settings": 82 | found = True 83 | print( "Rotors %s, ring %s, message key was %s, using crib %s" % (rotor_setting, ring_setting, start_pos, cribtext) ) 84 | 85 | if found == False: 86 | print( 'Attack unsuccessful' ) 87 | 88 | cluster.print_status() 89 | cluster.close() 90 | -------------------------------------------------------------------------------- /en/solutions/enigma_bf_canonical.py: -------------------------------------------------------------------------------- 1 | # This code attempts a partial brute force attack on Enigma messages. 2 | # Messages may be created on a real machine, compatible replica, the 3 | # Cryptoy Android App or any application that accurately reproduces 4 | # the 3-rotor machine used by the german armed forces. We have used 5 | # Brian Neal's Py-enigma Python3 library and utilities. 6 | # 7 | # This code uses Dispy on OctaPi using the recommended method for managing 8 | # jobs efficiently. For more information, visit the Dispy website. 9 | # 10 | 11 | # Dispy: 12 | # Giridhar Pemmasani, "dispy: Distributed and parallel Computing with/for Python", 13 | # http://dispy.sourceforge.net, 2016 14 | 15 | # Py-enigma: 16 | # Brian Neal 17 | # http://py-enigmareadthedocs.org/ 18 | # License: MIT License 19 | 20 | # All other original code: Crown Copyright 2016, 2017 21 | 22 | # Assumes message was encrypted as follows: 23 | # 24 | # STEP 1: Choose a start position and message key 25 | # where: UYT is initial rotor position (chosen 'randomly') 26 | # SCC is unencrypted message key (chosen 'randomly') 27 | # 28 | # STEP 2: Encrypyt the message key 29 | # pyenigma.py -r II V III -i 1 1 1 -p AV BS CG DL FU HZ IN KM OW RX -u B --start=UYT --text='SCC' 30 | # PWE 31 | # where: PWE is encrypted message key 32 | # 33 | # Note: We use a one-to-one relation for the rotor ring for compatability with Cryptoy 34 | # 35 | # STEP 3: Encrypt the message using the unencrypted message key 36 | # pyenigma.py --key-file=keys.txt --start='SCC' --text='THISXISXWORKING' 37 | # YJPYITREDSYUPIU 38 | # where: 39 | # SCC is the unencrypted message key 40 | # YJPYITREDSYUPIU is the cypher text produced 41 | # 42 | # STEP 4: Operator sends (usually in Morse): 43 | # STNA DE STNB 1104 = 15 = UYT PWE = BNUGZ YJPYI TREDS YUPIU 44 | # where: 45 | # STNA is callsign of destination station 46 | # DE means 'from' in Morse abreviation 47 | # STNB is callsign of originating station 48 | # = is the 'break' Morse character, which is used as a delimiter 49 | # 1104 is time message is sent (presumably UTC) 50 | # BNUGZ contains UGZ 'Kenngruppen' (day indicator for confirmation of correct key sheet entry on decrypt) 51 | # 52 | # 53 | 54 | ################################ 55 | # Brute Force attack 56 | # This is a limited brute force attack on the rotor settings assuming no plugboard and no rotor ring 57 | # We use 'THISXISXWORKING' as the crib message 58 | ################################ 59 | 60 | rotor = [ "I II III", "I II IV", "I II V", "I III II", 61 | "I III IV", "I III V", "I IV II", "I IV III", 62 | "I IV V", "I V II", "I V III", "I V IV", 63 | "II I III", "II I IV", "II I V", "II III I", 64 | "II III IV", "II III V", "II IV I", "II IV III", 65 | "II IV V", "II V I", "II V III", "II V IV", 66 | "III I II", "III I IV", "III I V", "III II I", 67 | "III II IV", "III II V", "III IV I", "III IV II", 68 | "III IV V", "IV I II", "IV I III", "IV I V", 69 | "IV II I", "IV II III", "IV I V", "IV II I", 70 | "IV II III", "IV II V", "IV III I", "IV III II", 71 | "IV III V", "IV V I", "IV V II", "IV V III", 72 | "V I II", "V I III", "V I IV", "V II I", 73 | "V II III", "V II IV", "V III I", "V III II", 74 | "V III IV", "V IV I", "V IV II", "V IV III" ] 75 | 76 | ring = [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", 77 | "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", 78 | "21", "22", "23", "24", "25", "26" ] 79 | 80 | # 81 | # This function does an exhaust search over the list of possible 82 | # rotor selections 83 | # 84 | def find_rotor_start( rotor_choice, ring_choice, ciphertext, cribtext ): 85 | 86 | from enigma.machine import EnigmaMachine 87 | 88 | alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 89 | 90 | machine = EnigmaMachine.from_key_sheet( 91 | rotors=rotor_choice, 92 | reflector='B', 93 | ring_settings=ring_choice, 94 | plugboard_settings='AV BS CG DL FU HZ IN KM OW RX') # plugboard known 95 | 96 | 97 | # do an exhaust search over all possible rotor starting positions 98 | for i in range(len(alphabet)): # search for rotor 1 start position 99 | for j in range(len(alphabet)): # search for rotor 2 start position 100 | for k in range(len(alphabet)): # search for rotor 3 start position 101 | # generate a possible rotor start position 102 | start_pos = alphabet[i] + alphabet[j] + alphabet[k] 103 | 104 | # set machine initial starting position and attempt decrypt 105 | machine.set_display(start_pos) 106 | plaintext = machine.process_text(ciphertext) 107 | 108 | # check if decrypt is the same as the crib text 109 | if (plaintext == cribtext): 110 | # print( start_pos, plaintext, cribtext ) 111 | return( rotor_choice, ring_choice, start_pos ) 112 | 113 | return( rotor_choice, ring_choice, "null" ) 114 | 115 | 116 | # main loop 117 | if __name__ == '__main__': 118 | import argparse, dispy, resource 119 | 120 | resource.setrlimit(resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY) ) 121 | resource.setrlimit(resource.RLIMIT_DATA, (resource.RLIM_INFINITY, resource.RLIM_INFINITY) ) 122 | 123 | parser = argparse.ArgumentParser() 124 | parser.add_argument("ciphertext", help="cipher text, which is the encrypted text to be broken") 125 | parser.add_argument("cribtext", help="crib text, which is known message content") 126 | parser.add_argument("ring_choice", help="slip ring setting to use, eg '1 1 1'") 127 | args = parser.parse_args() 128 | 129 | # extract the cipher and crib texts from the command line 130 | ciphertext = args.ciphertext 131 | cribtext = args.cribtext 132 | ring_choice = args.ring_choice 133 | 134 | cluster = dispy.JobCluster(find_rotor_start, nodes='192.168.1.*') 135 | jobs = [] 136 | id = 1 # job id 137 | 138 | print(( "Brute force crypt attack on Enigma message %s using crib %s" % (ciphertext, cribtext) )) 139 | 140 | # try all rotor settings (choosing three from five) 141 | print( 'Trying all rotor setings for ring choice "%s" ...' % (ring_choice) ) 142 | 143 | # submit the jobs for this ring choice 144 | for rotor_choice in rotor: 145 | job = cluster.submit( rotor_choice, ring_choice, ciphertext, cribtext ) 146 | job.id = id # associate an ID to the job 147 | jobs.append(job) 148 | id += 1 # next job 149 | 150 | 151 | print( "Waiting..." ) 152 | cluster.wait() 153 | print( "Collecting job results" ) 154 | 155 | # collect and check through the jobs for this ring setting 156 | found = False 157 | for job in jobs: 158 | rotor_setting, ring_setting, start_pos = job() # waits for job to finish and returns results 159 | if (start_pos != "null"): 160 | found = True 161 | print(( "Machine setting found: rotors %s, ring %s, message key was %s, using crib %s" % (rotor_setting, ring_setting, start_pos, cribtext) )) 162 | 163 | if (found == False): print( 'Attack unsuccessfull' ) 164 | 165 | cluster.print_status() 166 | cluster.close() 167 | -------------------------------------------------------------------------------- /en/solutions/enigma_bf_efficient.py: -------------------------------------------------------------------------------- 1 | # This code attempts a partial brute force attack on Enigma messages. 2 | # Messages may be created on a real machine, compatible replica, the 3 | # Cryptoy Android App or any application that accurately reproduces 4 | # the 3-rotor machine used by the german armed forces. We have used 5 | # Brian Neal's Py-enigma Python3 library and utilities. 6 | # 7 | # This code uses Dispy on OctaPi using the recommended method for managing 8 | # jobs efficiently. For more information, visit the Dispy website. 9 | # 10 | 11 | # Dispy: 12 | # Giridhar Pemmasani, "dispy: Distributed and parallel Computing with/for Python", 13 | # http://dispy.sourceforge.net, 2016 14 | 15 | # Py-enigma: 16 | # Brian Neal 17 | # http://py-enigmareadthedocs.org/ 18 | # License: MIT License 19 | 20 | # All other original code: Crown Copyright 2016, 2017 21 | 22 | 23 | # Assumes message was encrypted as follows: 24 | # 25 | # STEP 1: Choose a start position and message key 26 | # where: UYT is initial rotor position (chosen 'randomly') 27 | # SCC is unencrypted message key (chosen 'randomly') 28 | # 29 | # STEP 2: Encrypyt the message key 30 | # pyenigma.py -r II V III -i 1 1 1 -p AV BS CG DL FU HZ IN KM OW RX -u B --start=UYT --text='SCC' 31 | # PWE 32 | # where: PWE is encrypted message key 33 | # 34 | # Note: We use a one-to-one relation for the rotor ring for compatability with Cryptoy 35 | # 36 | # STEP 3: Encrypt the message using the unencrypted message key 37 | # pyenigma.py --key-file=keys.txt --start='SCC' --text='THISXISXWORKING' 38 | # YJPYITREDSYUPIU 39 | # where: 40 | # SCC is the unencrypted message key 41 | # YJPYITREDSYUPIU is the cypher text produced 42 | # 43 | # STEP 4: Operator sends (usually in Morse): 44 | # STNA DE STNB 1104 = 15 = UYT PWE = BNUGZ YJPYI TREDS YUPIU 45 | # where: 46 | # STNA is callsign of destination station 47 | # DE means 'from' in Morse abreviation 48 | # STNB is callsign of originating station 49 | # = is the 'break' Morse character, which is used as a delimiter 50 | # 1104 is time message is sent (presumably UTC) 51 | # BNUGZ contains UGZ 'Kenngruppen' (day indicator for confirmation of correct key sheet entry on decrypt) 52 | # 53 | # 54 | 55 | ################################ 56 | # Brute Force attack 57 | # This is a limited brue force attack on the rotor settings assuming no plugboard and no rotor ring 58 | # We use 'THISXISXWORKING' as the crib message 59 | ################################ 60 | 61 | # setup machine to be the same as that used for encrypt 62 | 63 | character = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 64 | rotor = [ "I II III", "I II IV", "I II V", "I III II", 65 | "I III IV", "I III V", "I IV II", "I IV III", 66 | "I IV V", "I V II", "I V III", "I V IV", 67 | "II I III", "II I IV", "II I V", "II III I", 68 | "II III IV", "II III V", "II IV I", "II IV III", 69 | "II IV V", "II V I", "II V III", "II V IV", 70 | "III I II", "III I IV", "III I V", "III II I", 71 | "III II IV", "III II V", "III IV I", "III IV II", 72 | "III IV V", "IV I II", "IV I III", "IV I V", 73 | "IV II I", "IV II III", "IV I V", "IV II I", 74 | "IV II III", "IV II V", "IV III I", "IV III II", 75 | "IV III V", "IV V I", "IV V II", "IV V III", 76 | "V I II", "V I III", "V I IV", "V II I", 77 | "V II III", "V II IV", "V III I", "V III II", 78 | "V III IV", "V IV I", "V IV II", "V IV III" ] 79 | 80 | ring = [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", 81 | "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", 82 | "21", "22", "23", "24", "25", "26" ] 83 | 84 | # 85 | # This function does an exhaust search over the list of possible 86 | # rotor selections 87 | # 88 | def find_rotor_start( rotor_choice, ring_choice, ciphertext, cribtext ): 89 | 90 | from enigma.machine import EnigmaMachine 91 | 92 | alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 93 | 94 | machine = EnigmaMachine.from_key_sheet( 95 | rotors=rotor_choice, 96 | reflector='B', 97 | ring_settings=ring_choice, 98 | plugboard_settings='AV BS CG DL FU HZ IN KM OW RX') # plugboard known 99 | 100 | 101 | # do an exhaust search over all possible rotor starting positions 102 | for i in range(len(alphabet)): # search for rotor 1 start position 103 | for j in range(len(alphabet)): # search for rotor 2 start position 104 | for k in range(len(alphabet)): # search for rotor 3 start position 105 | # generate a possible rotor start position 106 | start_pos = alphabet[i] + alphabet[j] + alphabet[k] 107 | 108 | # set machine initial starting position and attempt decrypt 109 | machine.set_display(start_pos) 110 | plaintext = machine.process_text(ciphertext) 111 | 112 | # check if decrypt is the same as the crib text 113 | if (plaintext == cribtext): 114 | # print( start_pos, plaintext, cribtext ) 115 | return( rotor_choice, ring_choice, start_pos ) 116 | 117 | return( rotor_choice, ring_choice, "null" ) 118 | 119 | 120 | 121 | # dispy calls this function to indicate change in job status 122 | def job_callback(job): # executed at the client 123 | global pending_jobs, jobs_cond 124 | global found 125 | 126 | if (job.status == dispy.DispyJob.Finished # most usual case 127 | or job.status in (dispy.DispyJob.Terminated, dispy.DispyJob.Cancelled, 128 | dispy.DispyJob.Abandoned)): 129 | # 'pending_jobs' is shared between two threads, so access it with 130 | # 'jobs_cond' (see below) 131 | jobs_cond.acquire() 132 | if job.id: # job may have finished before 'main' assigned id 133 | pending_jobs.pop(job.id) 134 | 135 | # extract the results for each job as it happens 136 | rotor_choice, ring_choice, start_pos = job.result # returns results from job 137 | if (start_pos != "null"): 138 | found = True 139 | dispy.logger.info( 'Machine setting found: job "%i" returned "%s" with ring "%s" using "%s", %s jobs pending', job.id, rotor_choice, ring_choice, start_pos, len(pending_jobs) ) 140 | 141 | if len(pending_jobs) <= lower_bound: 142 | jobs_cond.notify() 143 | 144 | jobs_cond.release() 145 | 146 | 147 | # main loop 148 | if __name__ == '__main__': 149 | import dispy, argparse, resource, threading, logging 150 | 151 | # set lower and upper bounds as appropriate 152 | # lower_bound is at least num of cpus and upper_bound is roughly 3x lower_bound 153 | # lower_bound, upper_bound = 352, 1056 154 | lower_bound, upper_bound = 32, 96 155 | 156 | resource.setrlimit(resource.RLIMIT_STACK, (resource.RLIM_INFINITY, resource.RLIM_INFINITY) ) 157 | resource.setrlimit(resource.RLIMIT_DATA, (resource.RLIM_INFINITY, resource.RLIM_INFINITY) ) 158 | 159 | parser = argparse.ArgumentParser() 160 | parser.add_argument("ciphertext", help="cipher text, which is the encrypted text to be broken") 161 | parser.add_argument("cribtext", help="crib text, which is known message content") 162 | args = parser.parse_args() 163 | 164 | # extract the cipher and crib texts from the command line 165 | ciphertext = args.ciphertext 166 | cribtext = args.cribtext 167 | 168 | server_nodes ='192.168.1.*' 169 | 170 | # use Condition variable to protect access to pending_jobs, as 171 | # 'job_callback' is executed in another thread 172 | jobs_cond = threading.Condition() 173 | 174 | pending_jobs = {} 175 | cluster = dispy.JobCluster(find_rotor_start, nodes=server_nodes, callback=job_callback, loglevel=logging.INFO) 176 | 177 | print( "Brute force crypt attack on Enigma message %s using crib %s" % (ciphertext, cribtext) ) 178 | 179 | # try all rotor settings (choosing three from five) 180 | found = False 181 | i = 1 # job counter 182 | r0 = 0 # ring 0 counter 183 | r1 = 0 # ring 1 counter 184 | r2 = 0 # ring 2 counter 185 | while (r0 < len(ring)) and (found == False): 186 | while (r1 < len(ring)) and (found == False): 187 | while (r2 < len(ring)) and (found == False): 188 | 189 | ring_choice = ring[r0] + " " + ring[r1] + " " + ring[r2] 190 | rot = 1 # rotor combination counter 191 | 192 | while (rot < len(rotor)) and (found == False): 193 | rotor_choice = rotor[rot] 194 | 195 | print( 'Trying rotor "%s" with ring "%s" ...' % (rotor_choice, ring_choice) ) 196 | 197 | # schedule execution of find_rotor_start (running 'dispynode') 198 | job = cluster.submit( rotor_choice, ring_choice, ciphertext, cribtext ) 199 | 200 | jobs_cond.acquire() 201 | 202 | job.id = i # associate an ID to the job 203 | 204 | # there is a chance the job may have finished and job_callback called by 205 | # this time, so put it in 'pending_jobs' only if job is pending 206 | if job.status == dispy.DispyJob.Created or job.status == dispy.DispyJob.Running: 207 | pending_jobs[i] = job 208 | # dispy.logger.info('job "%s" submitted: %s', i, len(pending_jobs)) 209 | if len(pending_jobs) >= upper_bound: 210 | while len(pending_jobs) > lower_bound: 211 | jobs_cond.wait() 212 | jobs_cond.release() 213 | 214 | rot += 1 # next rotor combination 215 | 216 | i += 1 # next job 217 | r2 += 1 # next ring 2 setting 218 | r1 += 1 # next ring 1 setting 219 | r0 += 1 # next ring 0 setting 220 | 221 | cluster.wait() 222 | 223 | if (found == False): print( 'Attack unsuccessfull' ) 224 | 225 | cluster.print_status() 226 | cluster.close() 227 | -------------------------------------------------------------------------------- /en/solutions/enigma_bf_standalone.py: -------------------------------------------------------------------------------- 1 | # This code attempts a partial brute force attack on Enigma messages. 2 | # Messages may be created on a real machine, compatible replica, the 3 | # Cryptoy Android App or any application that accurately reproduces 4 | # the 3-rotor machine used by the german armed forces. We have used 5 | # Brian Neal's Py-enigma Python3 library and utilities. 6 | # 7 | # This code runs standalone on the client and allows you to compare 8 | # compute runtime with the Dispy version running on OctaPi. 9 | # 10 | 11 | # Dispy: 12 | # Giridhar Pemmasani, "dispy: Distributed and parallel Computing with/for Python", 13 | # http://dispy.sourceforge.net, 2016 14 | 15 | # Py-enigma: 16 | # Brian Neal 17 | # http://py-enigmareadthedocs.org/ 18 | # License: MIT License 19 | 20 | # All other original code: Crown Copyright 2016, 2017 21 | 22 | # Assumes message was encrypted as follows: 23 | # 24 | # STEP 1: Choose a start position and message key 25 | # where: UYT is initial rotor position (chosen 'randomly') 26 | # SCC is unencrypted message key (chosen 'randomly') 27 | # 28 | # STEP 2: Encrypyt the message key 29 | # pyenigma.py -r II V III -i 1 1 1 -p AV BS CG DL FU HZ IN KM OW RX -u B --start=UYT --text='SCC' 30 | # PWE 31 | # where: PWE is encrypted message key 32 | # 33 | # Note: We use a one-to-one relation for the rotor ring for compatability with Cryptoy 34 | # 35 | # STEP 3: Encrypt the message using the unencrypted message key 36 | # pyenigma.py --key-file=keys.txt --start='SCC' --text='THISXISXWORKING' 37 | # YJPYITREDSYUPIU 38 | # where: 39 | # SCC is the unencrypted message key 40 | # YJPYITREDSYUPIU is the cypher text produced 41 | # 42 | # STEP 4: Operator sends (usually in Morse): 43 | # STNA DE STNB 1104 = 15 = UYT PWE = BNUGZ YJPYI TREDS YUPIU 44 | # where: 45 | # STNA is callsign of destination station 46 | # DE means 'from' in Morse abreviation 47 | # STNB is callsign of originating station 48 | # = is the 'break' Morse character, which is used as a delimiter 49 | # 1104 is time message is sent (presumably UTC) 50 | # BNUGZ contains UGZ 'Kenngruppen' (day indicator for confirmation of correct key sheet entry on decrypt) 51 | # 52 | # 53 | 54 | ################################ 55 | # Brute Force attack 56 | # This is a limited brue force attack on the rotor settings assuming no plugboard and no rotor ring 57 | # We use 'THISXISXWORKING' as the crib message 58 | ################################ 59 | 60 | rotor = [ "I II III", "I II IV", "I II V", "I III II", 61 | "I III IV", "I III V", "I IV II", "I IV III", 62 | "I IV V", "I V II", "I V III", "I V IV", 63 | "II I III", "II I IV", "II I V", "II III I", 64 | "II III IV", "II III V", "II IV I", "II IV III", 65 | "II IV V", "II V I", "II V III", "II V IV", 66 | "III I II", "III I IV", "III I V", "III II I", 67 | "III II IV", "III II V", "III IV I", "III IV II", 68 | "III IV V", "IV I II", "IV I III", "IV I V", 69 | "IV II I", "IV II III", "IV I V", "IV II I", 70 | "IV II III", "IV II V", "IV III I", "IV III II", 71 | "IV III V", "IV V I", "IV V II", "IV V III", 72 | "V I II", "V I III", "V I IV", "V II I", 73 | "V II III", "V II IV", "V III I", "V III II", 74 | "V III IV", "V IV I", "V IV II", "V IV III" ] 75 | 76 | # 77 | # This function does an exhaust search over the list of possible 78 | # rotor selections 79 | # 80 | def find_rotor_start( rotor_choice, ciphertext, cribtext ): 81 | 82 | from enigma.machine import EnigmaMachine 83 | 84 | alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 85 | 86 | machine = EnigmaMachine.from_key_sheet( 87 | rotors=rotor_choice, 88 | reflector='B', 89 | ring_settings='1 1 1', # no ring setting 90 | plugboard_settings='AV BS CG DL FU HZ IN KM OW RX') # plugboard known 91 | 92 | 93 | # do an exhaust search over all possible rotor starting positions 94 | for i in range(len(alphabet)): # search for rotor 1 start position 95 | for j in range(len(alphabet)): # search for rotor 2 start position 96 | for k in range(len(alphabet)): # search for rotor 3 start position 97 | # generate a possible rotor start position 98 | start_pos = alphabet[i] + alphabet[j] + alphabet[k] 99 | 100 | # set machine initial starting position and attempt decrypt 101 | machine.set_display(start_pos) 102 | plaintext = machine.process_text(ciphertext) 103 | 104 | # check if decrypt is the same as the crib text 105 | if (plaintext == cribtext): 106 | # print( start_pos, plaintext, cribtext ) 107 | return( rotor_choice, start_pos ) 108 | 109 | return( rotor_choice, "null" ) 110 | 111 | 112 | # main loop 113 | if __name__ == '__main__': 114 | import argparse 115 | 116 | parser = argparse.ArgumentParser() 117 | parser.add_argument("ciphertext", help="cipher text, which is the encrypted text to be broken") 118 | parser.add_argument("cribtext", help="crib text, which is known message content") 119 | args = parser.parse_args() 120 | 121 | # extract the cipher and crib texts from the command line 122 | ciphertext = args.ciphertext 123 | cribtext = args.cribtext 124 | 125 | print(( "Brute force crypt attack on Enigma message %s using crib %s" % (ciphertext, cribtext) )) 126 | 127 | # try all rotor settings (choosing three from five) 128 | for rotor_setting in rotor: 129 | print(( "Trying rotors %s..." % (rotor_setting) )) 130 | rotor_choice, start_pos = find_rotor_start( rotor_setting, ciphertext, cribtext ) 131 | if (start_pos != "null"): 132 | print(( "Machine setting found: rotors %s, message key was %s, using crib %s" % (rotor_choice, start_pos, cribtext) )) 133 | exit(0) 134 | -------------------------------------------------------------------------------- /en/solutions/enigma_test.py: -------------------------------------------------------------------------------- 1 | # This code is the original example provided with the Py-enigma 2 | # Python3 library and utilities. It is included here to confirm 3 | # correct operation of Py-enigma on your system before proceeding 4 | # with the brute force attack code. 5 | # 6 | 7 | # Py-enigma: 8 | # Brian Neal 9 | # http://py-enigmareadthedocs.org/ 10 | # License: MIT License 11 | 12 | # All other original code: Crown Copyright 2016, 2017 13 | 14 | # Assumes message was encrypted as follows: 15 | # 16 | # STEP 1: Choose a start position and message key 17 | # where: UYT is initial rotor position (chosen 'randomly') 18 | # SCC is unencrypted message key (chosen 'randomly') 19 | # 20 | # STEP 2: Encrypyt the message key 21 | # pyenigma.py -r II V III -i 1 1 1 -p AV BS CG DL FU HZ IN KM OW RX -u B --start=UYT --text='SCC' 22 | # PWE 23 | # where: PWE is encrypted message key 24 | # 25 | # Note: We use a one-to-one relation for the rotor ring for compatability with Cryptoy 26 | # 27 | # STEP 3: Encrypt the message using the unencrypted message key 28 | # pyenigma.py --key-file=keys.txt --start='SCC' --text='THISXISXWORKING' 29 | # YJPYITREDSYUPIU 30 | # where: 31 | # SCC is the unencrypted message key 32 | # YJPYITREDSYUPIU is the cypher text produced 33 | # 34 | # STEP 4: Operator sends (usually in Morse): 35 | # STNA DE STNB 1104 = 15 = UYT PWE = BNUGZ YJPYI TREDS YUPIU 36 | # where: 37 | # STNA is callsign of destination station 38 | # DE means 'from' in Morse abreviation 39 | # STNB is callsign of originating station 40 | # = is the 'break' Morse character, which is used as a delimiter 41 | # 1104 is time message is sent (presumably UTC) 42 | # BNUGZ contains UGZ 'Kenngruppen' (day indicator for confirmation of correct key sheet entry on decrypt) 43 | # 44 | # 45 | 46 | from enigma.machine import EnigmaMachine 47 | 48 | ################################ 49 | # Decrypt 50 | ################################ 51 | 52 | # setup machine to be the same as that used for encrypt 53 | 54 | machine = EnigmaMachine.from_key_sheet( 55 | rotors='II V III', 56 | reflector='B', 57 | ring_settings='1 1 1', 58 | plugboard_settings='AV BS CG DL FU HZ IN KM OW RX') 59 | 60 | # set machine initial starting position 61 | machine.set_display('UYT') 62 | 63 | # decrypt the message key 64 | msg_key = machine.process_text('PWE') 65 | 66 | # decrypt the cipher text with the unencrypted message key 67 | machine.set_display(msg_key) 68 | 69 | ciphertext = 'YJPYITREDSYUPIU' 70 | plaintext = machine.process_text(ciphertext) 71 | 72 | print(plaintext) 73 | -------------------------------------------------------------------------------- /en/step_1.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | Make and then break Enigma-encoded messages using your Raspberry Pi or [OctaPi](http://projects.raspberrypi.org/en/projects/build-an-octapi){:target="_blank"}. 4 | 5 | ### What you will make 6 | 7 | You will use the [Py-enigma](http://py-enigma.readthedocs.org/){:target="_blank"} library to create encrypted messages — secret messages only you and those you trust can read. You will then develop Python code to do a partial brute-force crypt attack on the Enigma messages and recover the machine's rotor settings. 8 | 9 | ![A GCHQ owned Enigma machine captured at the end of WWII](images/7X5A0933.jpg) 10 | 11 | ### What you will learn 12 | 13 | This project covers elements from the following strands of the [Raspberry Pi Digital Making Curriculum](http://rpf.io/curriculum){:target="_blank"}: 14 | 15 | + [Apply higher-order programming techniques to solve real-world problems](https://curriculum.raspberrypi.org/programming/maker/){:target="_blank"} 16 | 17 | ### Licence 18 | 19 | _OctaPi: brute-force Enigma_ by [GCHQ](https://www.gchq.gov.uk/){:target="_blank"} and the Raspberry Pi Foundation is licensed under a Creative Commons Attribution 4.0 International Licence. 20 | Based on a work at [github.com/raspberrypilearning/octapi-brute-force-enigma](https://github.com/raspberrypilearning/octapi-brute-force-enigma){:target="_blank"}. 21 | 22 | **Code and scripts copyright** 23 | [Crown Copyright](https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/){:target="_blank"} 24 | License: [Apache 2](https://www.apache.org/licenses/LICENSE-2.0){:target="_blank"} 25 | -------------------------------------------------------------------------------- /en/step_2.md: -------------------------------------------------------------------------------- 1 | ## What you will need 2 | 3 | ### Hardware 4 | 5 | + A Raspberry Pi 6 | 7 | and/or 8 | 9 | + An OctaPi 10 | 11 | ### Software 12 | 13 | You will need the [latest version of Raspbian](https://www.raspberrypi.org/downloads/), which already includes the following software package: 14 | 15 | - Python 3 16 | 17 | You will also need to install the `Py-enigma` module. 18 | 19 | [[[rpi-install-software]]] 20 | 21 | Type the following into a terminal window to install it: 22 | 23 | ```bash 24 | sudo pip3 install py-enigma 25 | ``` 26 | -------------------------------------------------------------------------------- /en/step_3.md: -------------------------------------------------------------------------------- 1 | ## What is Enigma, and how does it work? 2 | 3 | Enigma is a cipher machine that was created in the early 20th century for commercial, diplomatic, and military applications. During World War II, the machine was adopted by the German military for secret communications. The Enigma encryption code was famously broken during the war at Bletchley Park, the forerunner of GCHQ, meaning intercepted messages from the German military could be decoded and read. This spectacular achievement is thought to have shortened the war, saving many lives on both sides of the conflict. 4 | 5 | From an electrical point of view, the Enigma machine is simply a battery, 26 lightbulbs, and a switch circuit. It doesn't have any electronics, so it is an electro-mechanical device. Encryption is achieved by varying the path of an electric current through the wiring of the machine. 6 | 7 | ![Encoding a W as G on Enigma](images/Enigma-wiring.gif) 8 | 9 | In the diagram above, you can see how a letter typed on the keyboard goes through many stages of transposition before being routed to a lightbulb on the lamp board representing the encrypted letter. The user types their plain-text message on the keyboard character by character, and reads the cipher text as each bulb is illuminated on the lamp board in response. Due to the way transposition is achieved, a typed-in letter is never encrypted as itself (e.g. typing in A will never illuminate the lightbulb for A). 10 | 11 | The diagram might give you the impression that the transposition of letters is unchanging. This is not true though — how letters are transposed changes with every letter that is typed into the Enigma machine. That's what made the Enigma code so hard to break! The transposition changes because, as each letter is typed in, the path of the current changes as it flows to the bulbs. How does this work? 12 | 13 | ### Rotors and reflector 14 | 15 | Inside the machine, a number of rotors with 26 contacts (one for each letter from A to Z) are stacked together to create the current path through the heart of the machine. Each rotor wheel has 26 electrical contacts on both sides and a jumble of wiring on the inside, so that typed-in letters are transposed from one side to the other. In practice this means that a specific rotor transposes A into E, B into K, C into M, and so on. 16 | 17 | ![Close-up view of rotor from a WWII captured Enigma machine](images/7X5A0921-closeup.png) 18 | 19 | In the photo above, you can see the jumble of wiring inside an expanded rotor wheel from a WWII-captured Enigma machine. By stacking several rotors and using a reflector at the end to return the current back through the rotors, each letter is transposed many times. When using the Enigma machine, three rotors are selected from five available ones (there were also machines with four rotors). The reflector's transposition setting is fixed, ensuring that the current returns back through the machine without reversing the transposition. 20 | 21 | So how is the path of the current flowing through these components changed? 22 | 23 | ### Movement of the rotors 24 | 25 | So that a different transposition is used character by character, the first rotor advances step-wise as each letter of the message is typed, creating a new path for the current each time. As a result, the user can type in 'LL' and both letters will be encrypted differently, so the result might be 'XV'. After the first rotor has moved 26 positions (one full revolution), the machine starts advancing the next rotor position by position, and so on. 26 | 27 | ### Rotor start positions 28 | 29 | Part of what makes the Enigma encryption difficult to break is the fact that each rotor can be used at a different starting position. For example, if a rotor is set to position 10 at the beginning and the letter A is typed into the machine, it will enter not where A enters by default, but where J (letter 10 in the alphabet) enters by default. Note that a rotor will advance 26 steps no matter what its start position is. 30 | 31 | So that it can be set more easily, the rotor is marked by an alphabet ring. Hence a start position of 10 would be achieved by setting the rotor so that the letter J is visible; the 3-rotor start position "JFM" would mean setting the first rotor to J, the second to F, and the third to M. 32 | 33 | ### Slip rings 34 | 35 | On top of that, the letter assignments can be shifted by slipping round ring on the rotor. Rotating the slip ring rotates the wiring **inside** the rotor. For example, let's say that with the slip ring in default position, the rotor's wiring would transpose A into E, B into K, C into M, and so on. Moving the slip ring by 1 would mean that the letter A would be transposed into K, B into M, etc. 36 | 37 | ### Plugboard 38 | 39 | As if this wasn't enough, the German version of the Enigma machine also features a plugboard (the leftmost green box in the diagram at the top), which can be manually adjusted so that up to ten pairs of letters are transposed as they go into the rotors and again when they come back out. 40 | 41 | ### Encryption settings 42 | 43 | Combining three rotors from a set of five, the rotor settings with 26 positions, and the plugboard with ten pairs of letters connected, the Enigma machine used by WWII military had 158962555217826360000 (nearly 159 quintillion) different settings. 44 | 45 | The encryption relied on both the sending and receiving Enigma machines being set the same. To do this, secret identical settings sheets were used at both the sending and receiving communicating stations. The sheets specified: 46 | - Which rotors should be selected, and in what order they should be inserted into the machine 47 | - How much each rotor should be slipped round 48 | - Which letters should be changed by the plugboard 49 | - Which rotor start positions should be used 50 | 51 | Different machine settings was used each day, and the rotor start positions were even changed every six hours, so the machine setting was highly time-sensitive. This is also why the settings sheets the military handed out were so carefully guarded. 52 | 53 | ![A captured Enigma settings sheet held by GCHQ](images/Enigma-settings-sheet.jpg) 54 | 55 | ### Settings sheet 56 | 57 | This is an Enigma settings sheet captured at the end of WWII, which GCHQ has released for this project. In the expanded view of one of the lines shown below, you can see how the various settings are laid out: 58 | 59 | ![A line of settings from a WWII captured Enigma settings sheet](images/Enigma-settings-line.jpg) 60 | 61 | + The settings we've zoomed in on are for the first day of the month, hence the "1" in the second column from the left 62 | + The next column shows that rotors IV, I, and V should be selected and used in that order 63 | + The fourth column holds the slip ring settings: rotor IV should be slipped round to position 20, rotor I to position 5, and rotor V to position 10 64 | + Next comes the plugboard wiring: S to X, K to U, Q to F, and so on 65 | + Finally, the rotor start position for the four six-hour period of the day are "SRC", "EEJ, "FNZ", and "SZK" 66 | 67 | On top of that, there were two reflectors, B and C, one of which was chosen for use. For the encryption and decryption programs here, we will assume use of reflector B. 68 | 69 | ### One-off key 70 | 71 | For each message during WWII, the sender also selected three characters for themselves as a one-off message key — let's say "RPF". They encrypted this key using the settings from the settings sheet and noted down the result — let's say "QMD". They would then proceed to encrypt their message using their one-off key, here "RPF", as the start positions of the rotors, noting down the cypher text the machine returns. The encrypted version of the key, here "QMD", plus the cypher text were then sent to the recipient via radio. 72 | -------------------------------------------------------------------------------- /en/step_4.md: -------------------------------------------------------------------------------- 1 | ## Enigma during WWII 2 | 3 | In WWII, Enigma-encrypted messages were usually sent in Morse code via shortwave radio. This means they could easily be intercepted some distance away, so the German military relied heavily on the strength of the encryption technique to keep their messages secret. However, Britain intercepted and successfully decrypted the messages at Bletchley Park. 4 | 5 | An Enigma-encrypted transmission would have looked similar to this: 6 | 7 | ![Encrypted message](images/encrypted-message.png) 8 | 9 | How did operators at communications stations go about encrypting? 10 | 11 | ### Step 1: Select the rotors and choose a three-letter message key 12 | First, the operator found the line on the settings sheet that corresponds to the current day of the month. This told her how to set the Enigma machine, including which rotors to select and in what order to put them, as well as the rotor start position for the current six-hour period. 13 | 14 | ### Step 2: Choose and encrypt a three-letter message key 15 | The operator then picked a one-off three-letter message key, which should be random and unique to every single message. Let's say she thought of "SCC" as the key. Obviously, this key could not be sent openly. To encrypt it for transmission, the operator typed "SCC" into the Enigma machine she had set according to the sheet, and obtained (for example) "PWE" as the encrypted key. This key was then safe to send over a radio channel. 16 | 17 | For at least part of WWII, the German military procedure was to send and encrypt the message key twice. Using our example, the operator would have typed "SCCSCC" and obtained "PWEHVF". 18 | 19 | **There is a flaw with repeating the message key — what is it?** 20 | 21 | --- collapse --- 22 | --- 23 | title: Answer 24 | --- 25 | We previously said that no plain-text letter gets encrypted as itself. This means that anyone who intercepts an Enigma-encoded message knows that none of the letters in the decrypted message key can possibly be the right ones. In our example, intercepting the key "PWE" tells us that "P" is **not** the first letter, "W" is **not** the second, and "E" is **not** the third. 26 | 27 | If the message key is sent twice, as German military used to do, we also know the first letter cannot be "H", the second can't be "V", and the third can't be "F". This reduces the amount of searching we need to do to find the plain-text letters of the message key, because we can already exclude two options for each letter in the key. 28 | 29 | --- /collapse --- 30 | 31 | ### Step 3: Encrypt the message using the unencrypted message key 32 | Once the message key was chosen and encrypted, the operator set the rotors to the unencrypted version of the key that she had chosen, and typed the message into the keyboard. 33 | 34 | Numbers had to be spelled out in full, because the Engima machine doesn't have number keys. There was also no space bar, so a space was often indicated by an 'X'. For example, if we wanted to encrypt "this message is secret", we would type in "THISXMESSAGEXISXSECRET". 35 | 36 | ### Step 4: Send the encrypted message via radio 37 | A radio operator would then send the encrypted key and message in Morse code, using call signs and abbreviated text, just as we use abbreviations in text messages to reduce the amount of typing needed. 38 | -------------------------------------------------------------------------------- /en/step_5.md: -------------------------------------------------------------------------------- 1 | ## Encrypt a message 2 | 3 | Now let's use your Python-powered Enigma machine to create a secret message! 4 | 5 | + Open IDLE, create a new file, and save it as `encrypt.py`. 6 | 7 | [[[rpi-gui-idle-opening]]] 8 | 9 | + First, import the `EnigmaMachine` class from `Py-enigma` by adding this code to your file: 10 | 11 | ```python 12 | from enigma.machine import EnigmaMachine 13 | ``` 14 | Consulting your Enigma settings sheet, you find out that the settings for today are as follows: 15 | 16 | ![Encrypt settings](images/encrypt-settings.png) 17 | 18 | + In your Python file, set up an `EnigmaMachine` object using the settings from your settings sheet. Each setting should be a **string** and should be typed exactly as it appears on the settings sheet. For example, the `rotors` will be set as `'IV I V'`. 19 | 20 | ```python 21 | # Set up the Enigma machine 22 | machine = EnigmaMachine.from_key_sheet( 23 | rotors='', 24 | reflector='B', 25 | ring_settings='', 26 | plugboard_settings='') 27 | ``` 28 | 29 | As we said, we'll be using reflector B for all of our programs. 30 | 31 | + Write another line of code to set the rotor start positions to the settings from the sheet. 32 | 33 | ```python 34 | # Set the initial position of the Enigma rotors 35 | machine.set_display('FNZ') 36 | ``` 37 | 38 | + Choose three random letters to use as your message key — we will use "BFR", but you can choose whatever you like. Encrypt the message key and make a note of the result. This is the encrypted key you will send with your message. 39 | 40 | ```python 41 | # Encrypt the text 'BFR' and store it as msg_key 42 | msg_key = machine.process_text('BFR') 43 | print(msg_key) 44 | ``` 45 | 46 | + Write a line of code to reset the rotor start positions to your **unencrypted** message key (in our example, "BFR"). 47 | 48 | + Write some code to process the `plaintext` "RASPBERRYPI" and display the resulting `ciphertext`. 49 | 50 | --- hints --- 51 | --- hint --- 52 | Here is the code you used to encrypt the message key "BFR". Can you alter this code to encrypt your plaintext `"RASPBERRYPI"`? 53 | 54 | ```python 55 | msg_key = machine.process_text('BFR') 56 | ``` 57 | --- /hint --- 58 | --- hint --- 59 | Here is the code you will need: 60 | 61 | ```python 62 | plaintext = "RASPBERRYPI" 63 | ciphertext = machine.process_text(plaintext) 64 | print(ciphertext) 65 | 66 | ``` 67 | --- /hint --- 68 | --- /hints --- 69 | 70 | If you used the message key "BFR", the resulting ciphertext should be "GON XXLXYFQNZIK". If you've chosen a different message key, your result will be different. 71 | 72 | You can also run `pyenigma` from the command line if you wish. If you type this command into a terminal window, it will produce the same result as the script you just wrote: 73 | 74 | ```bash 75 | pyenigma.py -r IV I V -i 20 5 10 -p SX KU QP VN JG TC LA WM OB ZF -u B --start BFR --text "RASPBERRYPI" 76 | ``` 77 | 78 | **Do any of the characters ever get encrypted as themselves (i.e. does A get encrypted as A, B as B, etc.)?** 79 | 80 | --- collapse --- 81 | --- 82 | title: Answer 83 | --- 84 | No. This is in fact a weakness of the Enigma system, because, as we said, an attacker who wants to break a code can eliminate all possible crypt attack solutions where an A is decrypted as an A, and so on. 85 | 86 | --- /collapse --- 87 | 88 | 89 | ### Challenge 90 | 91 | + Encrypt the text using different settings from this authentic Enigma settings sheet, and see how the result changes. 92 | 93 | ![A captured Enigma settings sheet held by GCHQ](images/Enigma-settings-sheet.jpg) 94 | -------------------------------------------------------------------------------- /en/step_6.md: -------------------------------------------------------------------------------- 1 | ## Decrypt a message 2 | 3 | Imagine you are an Enigma operator and you've just received this message: 4 | 5 | ![Encrypted message](images/encrypted-message.png) 6 | 7 | Let's write some code using `Py-enigma` to simulate using an Enigma machine to decrypt the message. 8 | 9 | + Open IDLE, create a new file, and save it as `decrypt.py`. 10 | 11 | Consulting your Enigma settings sheet, you find out that the encrypting machine had the following settings at the time it sent the message: 12 | 13 | ![Decrypt settings](images/decrypt-settings.png) 14 | 15 | + Inside your file, import the `EnigmaMachine` class, and set up the `machine` with the settings shown on the settings sheet. Like last time, use reflector B. 16 | 17 | + Add some code to set the initial positions of the rotors to `U`, `Y`, and `T` to match the sending machine. 18 | 19 | The other operator sent you "PWE" as the key for this message. Before sending, the key was encrypted to prevent an eavesdropper from being able to read it. 20 | 21 | You first need to use your Enigma machine to recover the **actual** message key by decrypting "PWE" using the settings sheet's rotor start positions: `U`, `Y`, and `T`. 22 | 23 | + Add the following code to decrypt the key, and run your program to display result: 24 | 25 | ```python 26 | # Decrypt the text 'PWE' and store it as msg_key 27 | msg_key = machine.process_text('PWE') 28 | print(msg_key) 29 | ``` 30 | 31 | + Add some code at the bottom of the program to set the Enigma machine's rotor starting positions to the decrypted message key you just obtained. 32 | 33 | --- hints --- 34 | --- hint --- 35 | Look at how you originally set the rotor positions to `UYT`, and see if you can use this code to set the rotor positions to the new setting. 36 | --- /hint --- 37 | --- hint --- 38 | Here is how your code should look: 39 | 40 | ```python 41 | # Set the new start position of the Enigma rotors 42 | machine.set_display(msg_key) 43 | ``` 44 | --- /hint --- 45 | --- /hints --- 46 | 47 | You are now ready to decrypt the message. 48 | 49 | + Write some code to decrypt the cipher text `YJPYITREDSYUPIU`. 50 | 51 | --- hints --- 52 | --- hint --- 53 | This code will be very similar to the code you used to decrypt the key. Create a **variable** to store the result, use the `machine` to process the cipher text, and then `print` the result. 54 | --- /hint --- 55 | --- hint --- 56 | Here is how your code should look: 57 | 58 | ```python 59 | ciphertext = 'YJPYITREDSYUPIU' 60 | plaintext = machine.process_text(ciphertext) 61 | 62 | print(plaintext) 63 | ``` 64 | --- /hint --- 65 | --- /hints --- 66 | 67 | --- collapse --- 68 | --- 69 | title: What is the decrypted message? 70 | --- 71 | If all is well, you should see the script exiting without any errors, and the decrypted message "THISXISXWORKING". 72 | 73 | --- /collapse --- 74 | 75 | 76 | **What do you notice about the processes of encrypting and decrypting text?** 77 | 78 | --- collapse --- 79 | --- 80 | title: Answer 81 | --- 82 | They are exactly the same! The code you wrote in this section is identical to the code you wrote to encrypt the message. 83 | 84 | --- /collapse --- 85 | -------------------------------------------------------------------------------- /en/step_7.md: -------------------------------------------------------------------------------- 1 | ## Standalone crypt attack 2 | 3 | During WWII, cryptographers at Bletchley Park were working hard to try to break the Enigma cipher by hand to decrypt intercepted German ciphers. In this section, we will develop a brute-force crypt attack on the Enigma cipher text using a Raspberry Pi. 4 | 5 | A brute-force attack is simply an exhaustive search over all possible machine settings to try and find which one was used. For the time being, we will assume we know the plugboard settings. 6 | 7 | Here is the intercepted message we need to decipher: 8 | 9 | ![Intercepted message](images/intercepted-message.png) 10 | 11 | In addition to our cipher text, we will use a crib text, which is a guess at what the cipher text might be. This may seem like a cheat, but is actually exploiting a weakness of the Enigma system as used during WWII: some of message text was predictable, especially its beginning. For example, weather report messages were a good source of cribs, as they often contained the word "WETTER", the German word for "weather". 12 | 13 | We will use this `cribtext` to help us launch the brute-force attack: 14 | 15 | ```python 16 | ciphertext = "YJPYITREDSYUPIU" 17 | cribtext = "THISXISXWORKING" 18 | ``` 19 | 20 | We will know that the attack has found the correct rotor choices and starting positions when the cipher text is decrypted to the crib text. 21 | 22 | + Create a new Python file and save it as `bruteforce_standalone.py`. 23 | 24 | + Add the variables containing the cipher text and the crib text as strings. 25 | 26 | We need to represent the selection of three out of five rotor wheels in our Python code. We could write code to generate the possibilities, but as there aren't very many, we can manually define them. 27 | 28 | + Copy this list of all possible rotor choices into your file: 29 | 30 | ```python 31 | rotors = [ "I II III", "I II IV", "I II V", "I III II", 32 | "I III IV", "I III V", "I IV II", "I IV III", 33 | "I IV V", "I V II", "I V III", "I V IV", 34 | "II I III", "II I IV", "II I V", "II III I", 35 | "II III IV", "II III V", "II IV I", "II IV III", 36 | "II IV V", "II V I", "II V III", "II V IV", 37 | "III I II", "III I IV", "III I V", "III II I", 38 | "III II IV", "III II V", "III IV I", "III IV II", 39 | "III IV V", "IV I II", "IV I III", "IV I V", 40 | "IV II I", "IV II III", "IV I V", "IV II I", 41 | "IV II III", "IV II V", "IV III I", "IV III II", 42 | "IV III V", "IV V I", "IV V II", "IV V III", 43 | "V I II", "V I III", "V I IV", "V II I", 44 | "V II III", "V II IV", "V III I", "V III II", 45 | "V III IV", "V IV I", "V IV II", "V IV III" ] 46 | ``` 47 | 48 | Our strategy will be to select each set of rotor choices in the `rotors` list in turn and check to see whether decrypting the cipher text with this combination of rotors obtains the crib text. 49 | 50 | However, it is not as simple as just testing every single possible choice of rotors. Inside our function, we will also need to search through all possible rotor start positions for each rotor combination. 51 | 52 | For the time being, we will assume the slip ring settings "1 1 1" and the plugboard settings "AV BS CG DL FU HZ IN KM OW RX" — we'll discuss adding these later. The code breakers at Bletchley Park would not have had this luxury! 53 | 54 | + Create a function called `find_rotor_start()` which takes three arguments: the rotor choice, the cipher text, and the crib text. 55 | 56 | [[[generic-python-simple-functions]]] 57 | 58 | + Inside your function, add the code to import the `EnigmaMachine` class: 59 | 60 | ```python 61 | from enigma.machine import EnigmaMachine 62 | ``` 63 | 64 | We have imported the `Py-Enigma` module **inside** our function for a reason: this allows us to reuse this code later on with an OctaPi cluster, on which we can run the script massively in parallel, and thus in much shorter time than on a single processor. 65 | 66 | + Write code inside the function to test all possible rotor start positions for the given rotor choice. Remember that we are passing a rotor choice into the function, so you only need to test all start positions for the **specified** rotor choice, not for every possible rotor choice! 67 | 68 | --- hints --- 69 | --- hint --- 70 | + Import the `EnigmaMachine` class. 71 | 72 | + Create a string to store the alphabet so that you can easily loop through the letters. 73 | 74 | + Set up your `EnigmaMachine` object just as we did before. Use reflector B and the slip ring and plugboard settings mentioned above. 75 | 76 | + Loop through the alphabet to generate all possible start positions for each rotor. For example, if all rotors begin on A, the first start position to test might be AAA. The second might be AAB, then AAC, and so on, until rotor 3 reaches the end of the alphabet. Then move rotor 2 on one position, reset rotor 3, and begin incrementing it again, resulting in ABA, then ABB, ABC, etc. 77 | 78 | + For each rotor start position, decrypt the given cipher text and check whether it is the same as the crib text, printing the resulting `plaintext` as you go along. If the cipher and crib texts are indeed the same, return the rotor choice and the start position. 79 | 80 | --- /hint --- 81 | --- hint --- 82 | Here is how your code might look: 83 | 84 | ```python 85 | def find_rotor_start( rotor_choice, ciphertext, cribtext ): 86 | from enigma.machine import EnigmaMachine 87 | alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 88 | 89 | machine = EnigmaMachine.from_key_sheet( 90 | rotors=rotor_choice, 91 | reflector='B', 92 | ring_settings='1 1 1', 93 | plugboard_settings='AV BS CG DL FU HZ IN KM OW RX') 94 | 95 | # Do a search over all possible rotor starting positions 96 | for rotor1 in alphabet: # Search for rotor 1 start position 97 | for rotor2 in alphabet: # Search for rotor 2 start position 98 | for rotor3 in alphabet: # Search for rotor 3 start position 99 | 100 | # Generate a possible rotor start position 101 | start_pos = rotor1 + rotor2 + rotor3 102 | 103 | # Set the starting position 104 | machine.set_display(start_pos) 105 | 106 | # Attempt to decrypt the plaintext 107 | plaintext = machine.process_text(ciphertext) 108 | print( plaintext ) 109 | 110 | # Check if decrypted version is the same as the crib text 111 | if plaintext == cribtext: 112 | print("Valid settings found!") 113 | return rotor_choice, start_pos 114 | 115 | # If we didn't manage to successfully decrypt the message 116 | return rotor_choice, "Cannot find settings" 117 | ``` 118 | --- /hint --- 119 | --- /hints --- 120 | 121 | Most of the time, our function will fail to match the cipher and crib texts, because the rotor choice will be wrong. In one instance (we hope!) the cipher and crib texts will match, because we have found the right machine setting. 122 | 123 | + In the main part of your program, write a loop to call the function once for every possible rotor choice combination in the `rotors` list. For each time the function is called, print out the results. If ever a start position is returned that is not "Cannot find settings", break out of the loop — the right settings have been found! 124 | 125 | --- hints --- 126 | --- hint --- 127 | Use a for loop to loop through the list and call the function once for each item in the list. 128 | --- /hint --- 129 | --- hint --- 130 | Here is how your code might look: 131 | 132 | ```python 133 | for rotor_setting in rotors: 134 | rotor_choice, start_pos = find_rotor_start( rotor_setting, ciphertext, cribtext ) 135 | print(rotor_choice + " " + start_pos ) 136 | if start_pos != "Cannot find settings": 137 | break 138 | ``` 139 | --- /hint --- 140 | --- /hints --- 141 | 142 | + Save and run your program. It will take quite a long time to run, but as it executes, you should see the results for each rotor choice. 143 | 144 | + Once your brute-force attack has found the rotors and start position, plug them into the `decrypt.py` program you wrote earlier and decrypt the full secret message! 145 | 146 | --- collapse --- 147 | --- 148 | title: Answer 149 | --- 150 | 151 | The settings were as follows: rotors = II V III / starting position = SCC 152 | The secret message reads `"THISXISXWORKINGXOCTAPIXISXAWESOME"` 153 | 154 | --- /collapse --- 155 | 156 | We did not code the rotors' slip ring settings. The slip ring shifts the wiring inside the rotor — slip it round by one and A connects to where B did before, B connects to where C did before, C connects to where D did before, and so on. To deal with the rotor ring setting, we would need to modify the `find_rotor_start()` function so that it runs repeatedly for every rotor slip ring setting. 157 | 158 | **How much longer will it take to run a program that searches over all possible slip ring settings as well?** 159 | 160 | --- collapse --- 161 | --- 162 | title: Answer 163 | --- 164 | Each Enigma machine rotor can have 26 slip ring positions: A to A (no shift), A to B (shift by 1), ..., A to Z (shift by 26). In an Enigma machine with three rotors, this means we would have to run the search for the slip ring position 26 times for the first rotor, and all of that 26 times for the second rotor, and all of that 26 times for the third rotor. So our brute-force crypt attack program will take `26 × 26 × 26 = 17576` times longer. 165 | 166 | This is a very long time, but we can break up the problem into many parts and run these in parallel using OctaPi. This is what we will do next! 167 | 168 | --- /collapse --- 169 | -------------------------------------------------------------------------------- /en/step_8.md: -------------------------------------------------------------------------------- 1 | ## Crypt attack using OctaPi 2 | 3 | This part of the resource requires an [OctaPi cluster](https://projects.raspberrypi.org/en/projects/build-an-octapi){:target="_blank"}. 4 | 5 | Before you begin, you will need to install `Py-enigma` on your client machine and all the servers in your OctaPi cluster. 6 | 7 | --- collapse --- 8 | --- 9 | title: Install `py-enigma` on the OctaPi client and servers 10 | --- 11 | 12 | + Boot up the client and connect it to the internet. This will mean disconnecting from your OctaPi router and connecting to your WiFi network for internet access. 13 | 14 | + Open the terminal. 15 | 16 | ![Open a terminal](images/terminal.png) 17 | 18 | + Type the following command into the terminal window: 19 | 20 | ```bash 21 | sudo pip3 install py-enigma 22 | ``` 23 | 24 | + Disconnect the OctaPi client from the internet and connect it to your dedicated OctaPi router again. 25 | 26 | + Type the following command into a terminal window to open the `wpa_supplicant.conf` file: 27 | 28 | ```bash 29 | sudo nano /etc/wpa_supplicant/wpa_supplicant.conf 30 | ``` 31 | 32 | + To avoid the client connecting to the wrong network, remove any entries in `wpa_supplicant.conf` that are for WiFi networks other than OctaPi. 33 | 34 | + Press `Ctrl` + `o` to save, and `Ctrl` + `x` to exit the text editor. 35 | 36 | Next, repeat the following process for each of the servers. 37 | 38 | + Select one server, then connect a keyboard, screen, and mouse to it so that you can administer it directly from a terminal window. Alternatively, place the SD card into a Raspberry Pi with connected peripherals. 39 | 40 | + Repeat all the steps needed to install `Py-enigma` that you followed for the client. 41 | 42 | + Shut down the server and either repeat the installation the same way for the rest of the servers in your cluster, or duplicate the SD card. 43 | 44 | --- /collapse --- 45 | 46 | To do an exhaustive search of all rotor slip ring settings, we will need to run a lot of jobs on OctaPi using Dispy, which you installed when you built the OctaPi. The OctaPi code using Dispy is very similar to the code we created for a standalone processor. 47 | 48 | The demand on the OctaPi client machine's memory will be quite high, so we will need to run the program one ring setting at a time. 49 | 50 | + Start with the code you wrote for the standalone attack, but save a copy of the file as `bruteforce_octapi.py`. 51 | 52 | + Open the file using Python 3 (IDLE) from the **Programming** menu. 53 | 54 | + Remove everything except the list of rotor permutations and the `find_rotor_start()` function. 55 | 56 | + Import `dispy` and `socket` at the top of the file. 57 | 58 | ```python 59 | import dispy, socket 60 | ``` 61 | 62 | + Alter the `find_rotor_start()` function so that it now takes an additional parameter: the `ring_choice`. This will be a **string** containing three numbers separated by spaces, for example `'1 1 1'`. 63 | 64 | + Inside the function, set the `ring_choice` in the `EnigmaMachine` object to be the `ring_choice` that was passed into the function. 65 | 66 | + Find the two places where a value is returned from the function (when a match has been found, or when all possibilities are exhausted and no match was found). In addition to returning the rotor choice and start position, add code to return the `ring_choice` as the **second** value returned, so that the function returns three values in total. 67 | 68 | + In the main part of your program (where your loop was in the standalone version), add some code to allow the user to input the cipher text, the crib text, and the ring setting. You could either do this via the `input()` function or by collecting the arguments from the command line with the `argparse` module. 69 | 70 | + Create a `cluster` object on the OctaPi network using the code below. If your OctaPi network uses a different IP address range to the default, you will need to alter the `nodes` part of the code to reflect this. 71 | 72 | ```python 73 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 74 | s.connect(("8.8.8.8", 80)) # doesn't matter if 8.8.8.8 can't be reached 75 | cluster = dispy.JobCluster(find_rotor_start, ip_addr=s.getsockname()[0], nodes='192.168.1.*') 76 | 77 | jobs = [] 78 | id = 1 79 | ``` 80 | 81 | + Submit the `find_rotor_start()` jobs to the cluster using a similar method to the loop you used in the standalone brute-force attack. 82 | 83 | ```python 84 | # Submit the jobs for this ring choice 85 | for rotor_choice in rotors: 86 | job = cluster.submit( rotor_choice, ciphertext, cribtext, ring_choice ) 87 | job.id = id # Associate an ID to the job 88 | jobs.append(job) 89 | id += 1 # Next job 90 | ``` 91 | 92 | + Next we need to wait for the jobs to complete before collecting the results the cluster has returned. 93 | 94 | ```python 95 | print( "Waiting..." ) 96 | cluster.wait() 97 | print( "Collecting job results" ) 98 | ``` 99 | 100 | + The last step is to sift through the results to see if any of the `find_rotor_start()` jobs did not return the string `"Cannot find settings"`, in which case the returned string must have been a valid rotor start position. 101 | 102 | ```python 103 | # Collect and check through the jobs for this ring setting 104 | found = False 105 | for job in jobs: 106 | # Wait for job to finish and return results 107 | rotor_setting, ring_setting, start_pos = job() 108 | 109 | # If a start position was found 110 | if start_pos != "Cannot find settings": 111 | found = True 112 | print( "Rotors %s, ring %s, message key was %s, using crib %s" % (rotor_setting, ring_setting, start_pos, cribtext) ) 113 | ``` 114 | 115 | + Lastly, you can tidy up and exit. 116 | 117 | ```python 118 | if found == False: 119 | print( 'Attack unsuccessful' ) 120 | 121 | cluster.print_status() 122 | cluster.close() 123 | ``` 124 | 125 | + Save and run your code from a terminal using the ciphertext `'FKFPQZYVON'` with the crib `'CHELTENHAM'` and ring settings `'1 1 1'`. The program should take around 30 seconds to complete. 126 | 127 | Here is an example of the program running with arguments passed from the command line: 128 | 129 | ![Running enigma_bf_canonical](images/enigma-canonical-qjf.png) 130 | 131 | **If you run the OctaPi code with different ring settings, do you sometimes get more than one result? Why is this?** 132 | 133 | --- collapse --- 134 | --- 135 | title: Answer 136 | --- 137 | 138 | For the same rotor selections, you sometimes find multiple valid machine settings with different ring settings and rotor start positions. For example, you could have found start positions "ABC" with ring settings "1 1 1", as well as "ABD" with "1 1 2", as two valid results. This isn't a bug: both machine settings are valid. In fact, there are multiple valid machine settings because moving the rotor's slip ring creates multiple equivalent crypt solutions. 139 | 140 | This isn't another example of a mistake in the Enigma encryption technique, but shows how the nature of the cyber threat has changed in the 75 years since WWII. Originally, the risk was posed by people decrypting letter by letter. Changing the rotor slip ring meant that the rotors advanced at unexpected positions, creating a discontinuity every 26, 26×26 and 26×26×26 characters, meaning that an attacker would have to keep starting again. With our Raspberry Pi–based crypt attack using a simple brute-force exhaust search over the full range of possible machine settings, we find that the rotor slip ring setting creates multiple valid solutions. So for us, this feature is a weakness because less searching is needed to find a valid solution. 141 | 142 | Here's an example (compare with the screenshot above): 143 | 144 | ![Running enigma_bf_canonical](images/enigma-canonical-qjg.png) 145 | 146 | We could have saved a lot of time coding the slip ring search if we had known this beforehand. 147 | 148 | --- /collapse --- 149 | 150 | **What is the minimum length of crib text needed to obtain the correct machine setting?** 151 | 152 | --- collapse --- 153 | --- 154 | title: Answer 155 | --- 156 | If you run your code multiple times with fewer and fewer crib and cipher text characters, you should find that as few as four characters of crib text is enough to obtain the correct machine setting (as well as a handful of incorrect solutions). With fewer than four characters, there is so much ambiguity that you will have trouble finding the correct solution amongst all the incorrect solutions. 157 | 158 | --- /collapse --- 159 | -------------------------------------------------------------------------------- /en/step_9.md: -------------------------------------------------------------------------------- 1 | ## Challenge: find out more 2 | 3 | If you want to know more about the story of breaking the Enigma code, the following book will be of interest: _Dilly — The Man Who Broke Enigmas_, Mavis Batey, ISBN 9781906447151, Biteback. 4 | 5 | ### Very advanced coding challenge 6 | You could try coding the search over plugboard settings. Before you start, try estimating if this is going to be achievable even with an OctaPi cluster. 7 | --------------------------------------------------------------------------------