“If you will not show us the grail, we shall take your castle by force!”
A brute force attack is the process of repeatedly attempting possible combinations to a lock or passcode until the desired outcome is reached. Have you ever tried all possible combinations on a bike lock after forgetting its combination? That’s an example of a brute force attack.
Other examples of brute force attacks include trying all possible PIN (personal identification) numbers to gain access to a cell phone or tablet. Criminals use brute force to determine ATM card PIN numbers. Brute force attacks have also been used as a means to determine encryption keys. In this tutorial, you will experiment with brute force attacks and defenses to counter them:
You will need:
Complete these tutorials first:
You will understand the basics of brute force attacks and be able to write scripts that perform those attacks. You will also be able to modify scripts in apps to strengthen their defenses against brute force attacks.
You will also be ready to move on to the next tutorials (coming soon!)
Imagine you are trying to open an old bike lock with 3 digits, each 0 to 9. That bike lock only has 1000 combinations (0 through 999). If it takes 3 seconds to test each combination, it would take 3000 seconds.
3000 seconds * (1 minute / 60 seconds) = 50 minutes.
In other words, your brute force attack would be guaranteed to succeed in just under an hour.
With four digits, the combinations increase to 10,000, and instead of 50 minutes, the same brute force attack could take up to 500 minutes.
Computer passwords usually must be at least 8 characters, and use a variety of them — letters, upper-case, lower-case, digits, punctuation, and symbols like # $ %, etc. With 94 possible printable characters, how many different combinations would that be?
94 x 94 x 94 x 94 x 94 x 94 x 94 x 94 = 948 = 6,095,689,385,410,816.
A person could spend an entire lifetime manually testing passwords, and still not crack the combination! Even a computer trying 1,000,000 different combinations per second could still take up to 193 days to find the right one.
Early examples of brute force attacks occured during WWII, when Allied Forces worked to crack ciphertext encrypted by German Enigma machines. These machines used a constantly shifting key from letter to letter. Due to the immense number of combinations, without a copy of the mechanisms that were being used, decryption by hand was impossible.
Museo della Scienza e della Tecnologia "Leonardo da Vinci" / CC BY-SA
A device known as the Bombe machine was invented to help crack the Enigma cipher. It systematically went through every combination of encrypted messages to reveal discernible original messages.
Antoine Taveneaux [4] / CC BY-SA [5]
This activity starts with a micro:bit PIN pad that you can type a PIN into using its A and B buttons. It radio transmits the PIN you entered to a micro:bit vault. Enter the correct PIN and access is granted. Enter the wrong one, and access is denied. In this starting activity, the PIN’s security is intentionally weak: three binary digits (0s or 1s).
This first application does not require the terminal, so the micro:bit modules will only be drawing power from the USB cable. The step-by-step instructions are the same as you have been following for terminal connected micro:bit modules that are communicating wirelessly. However, you can optionally load programs in one at a time, and connect both micro:bit modules to batteries, and the application will still work.
See Texting with Terminals [6] if you need a reminder on how to set up the serial connection to the micro:bit modules.
# pin_pad_transmitter.py from microbit import * import radio radio.on() radio.config(channel=7) sleep(1000) pin = '' n = 0 while True: x = len(pin) b = n * 9 display.set_pixel(x,4,9) for y in range(0, 4): display.set_pixel(x, y, b) if button_a.was_pressed(): n = n + 1 n = n % 2 if button_b.was_pressed(): pin += str(n) if x == 3: radio.send(pin) display.scroll(pin) pin = '' n = 0 display.clear()
# bank_vault_receiver from microbit import * import radio radio.on() radio.config(channel=7) sleep(1000) pin = '011' while True: display.set_pixel(2,2,9) message = radio.receive() if message: if message == pin: radio.send("Access granted.") for n in range(4): display.show(Image.YES) sleep(1000) display.clear() sleep(200) else: radio.send("Access denied.") display.show(Image.NO) sleep(3000) display.clear()
Before using the PIN Pad Transmitter micro:bit to open the Vault Receiver micro:bit, let’s check to make sure each one is running the correct script.
Here’s how to enter PINs into the PIN pad transmitter. Press/release the A button to select 0 (one light) or 1 (all 5 lights in the column). When you have the digit you want, press B to enter it and advance to the next digit and column. The script automatically copies the previous column’s digit to the next column. If that’s the digit you want, just press the B button again to accept it, or press the A button to change it before pressing the B button to accept it.
After you press B to accept the digit in the 3rd column, the script automatically transmits the PIN to the Bank Vault Receiver micro:bit. If the PIN is correct, it’ll display a check-mark. If it’s not correct, it’ll display an X. Let’s try entering the correct 0 1 1 PIN first to get the access granted checkmark from the bank vault’s display.
To enter and send 0 1 1, press/release the PIN Pad Transmitter micro:bit’s reset button, then press/release: B A B B.
Verify that the Vault Receiver micro:bit displays a check mark indicating access granted.
<<< enter-correct-pin-how-to >>>
It’s also important to make sure the vault works by not granting access to any other PIN.
How long does it take you to test through the possible PINs to get the correct response? (For a brute force attack, you would have to try 000, 001, 010, 011, 100, 101, 110, and 111.)
If you need any refreshers on the first few statements, check the Cybersecurity: Radio Basics tutorial’s Send Radio Messages [7] activity.
# pin_pad_transmitter.py from microbit import * import radio radio.on() radio.config(channel=7) sleep(1000)
Before entering the while True:… loop, the script starts by creating an empty string variable named pin, and an int variable named n (for number). The n variable will store the number you are currently entering with the micro:bit PIN pad’s A/B buttons. That number will either be 0 or 1.
pin = '' n = 0 while True:
Setting x equal to the length of the pin string is a way of keeping track of how many digits you have entered with the micro:bit module's A and B buttons. As you enter digits, they will be added to the pin string as '0' and '1' characters. With each extra character in the pin string, it increases the number of characters len(pin) returns by 1. The script will use this information to keep track of which column it’s displaying 0s and 1s in, and also to decide when to radio-transmit the pin string to the Vault micro:bit. For example, after you have pressed B a second time to enter the second digit, len(pin) will return 2 since the string now contains two digits.
x = len(pin)
At this point, n is either 0 or 1. The b = n * 9 statement sets the b int variable to 0 when n is 0, or 9 when n is 1. This is a way of setting LED brightness without relying on if n == 1: … else: …
b = n * 9
The display.set_pixel(x, y, b) method call sets the brightness of an individual micro:bit LED. The column is selected with x (0 to 4), the row with y (0 to 4), and the brightness with b (0 to 9). Here the script uses x (the number of digits in the pin string) to pick the column of LEDs to turn on and off. The LED in row 4 will be on regardless of whether the n is 0 or 1, and the brightness will be the maximum value 9.
display.set_pixel(x,4,9)
The other four LEDs in the x column can be turned on or off in a loop that counts y from 0 through 3. As the loop goes through those 4 LED y positions, it will turn them all off if b is 0, or all on to full brightness if b is 9.
for y in range(0, 4): display.set_pixel(x, y, b)
Next, the script checks if the A button was pressed since the last while True: loop repetition since you might have pressed the A button to either change the 0 in a given column to 1, or change the 1 in a given column to 0. When the A button is pressed, the if button_a.was_pressed(): statement first adds 1 to n with n = n + 1. After that, n = n % 2 uses the modulus operator to change n to 0 each time it reaches 2.
if button_a.was_pressed(): n = n + 1 n = n % 2
You have seen the modulus % operator used to wrap values back around to the beginning in the How Caesar Letter Encryption Works [8] section of Cybersecurity: Encryption Intro [2]. In the Caesar cipher, it made a character index variable wrap from 25 to 0 instead of reaching 26. Here, it makes the n variable wrap from 1 to 0 instead of reaching 2.
Pressing the B button is for entering the current digit and moving on to the next. But first, the script has to remember the current digit. It does so by converting that digit to a single character string with str(n). Since the result will either be '0' or '1', that’s the character that’ll be appended to the pin string with ping += str(n).
if button_b.was_pressed(): pin += str(n)
Next time the while True: loop repeats, x = len(pin) will have increased by 1 since the string just got longer by one character. Since the x value increased by 1, it advances the digit setting to the next column on the LED display.
After the 3rd digit has been appended to the pin string and it contains something like '011', x = len(pin) will set x to 3 causing the if x == 3: block of the script to be executed. This is where it sends that pin string to the Vault micro:bit via radio. It also resets pin to an empty string, n to 0, and clears the display.
if x == 3: radio.send(pin) display.scroll(pin) pin = '' n = 0 display.clear()
Aside from the comment with the script name, these statements match the pin_pad_transmitter script.
# bank_vault_receiver from microbit import * import radio radio.on() radio.config(channel=7) sleep(1000)
Before entering the while True: loop, the receiver has a hard-coded pin string of '011'. If the PIN Pad Transmitter micro:bit sends that value, it will grant access and display a checkmark. If not, it will deny access and display an X.
pin = '011'
The while True: loop rapidly cycles through three statements. First, it turns on the LED display’s middle LED pixel by setting column to 2 from left, row to 2 from top, and brightness to the maximum of 9 with display.set_pixel(2, 2, 9). Next, it stores whatever the radio might have received since the last loop repetition with message = radio.receive(). Most of the time, the result will be None, so everything below and indented from if message will be skipped.
while True:
display.set_pixel(2,2,9)
message = radio.receive()
if message:
The script assumes that if radio.receive() returns something other than None to message, it’ll be the string containing 3 digits. So, it compares message to pin with if message == pin:. If message stores '011', it will match pin. In that case, radio.send("Access granted.") sends a confirmation string and displays the checkmark with Image.YES. It flashes that on for 1 second, then off for 0.2 seconds, repeated four times.
if message == pin: radio.send("Access granted.") for n in range(4): display.show(Image.YES) sleep(1000) display.clear() sleep(200)
If the PIN is incorrect, the script sends an "Access denied." string through the radio, then it displays the X with Image.NO, and waits for 3 seconds. Note that the PIN pad does not currently pay attention to that wireless message, but other programs will.
else: radio.send("Access denied.") display.show(Image.NO) sleep(3000)
Before repeating the while True: loop and checking for more radio messages, the script clears the LED display.
display.clear()
A PIN with three binary digits has 8 possible combinations: 000, 001, 010, 011, 100, 101, 110, and 111. With that number, it’s not too time-consuming to brute force attack manually. Just hand-enter all the combinations and within a minute, you’ve got access.
With more possible digits, a PIN could end up taking hours, days, or even years to crack with brute force by hand. So, brute force attacks are often mounted by scripts—the attacker doesn’t have to spend all that time manually typing in different PIN values. The computer (or in our case, the micro:bit module) does the work, and the attacker can check back periodically to find out if the PIN has been cracked.
The Bank Vault Receiver micro:bit should still be on and running the bank_vault_receiver script.
# bank_vault_crack.py from microbit import * import radio radio.on() radio.config(channel=7) sleep(1000) digits = ['0','1'] display.show(Image.ARROW_W) while True: if button_a.was_pressed(): display.clear() for a in digits: for b in digits: for c in digits: pin = ''.join([a, b, c]) print("pin =", pin) for x in range(0, len(pin)): bit = int(pin[x]) brightness = bit * 9 display.set_pixel(x,4,9) for y in range(0, 4): display.set_pixel(x, y, brightness) response = None while response is None: radio.send(pin) sleep(100) response = radio.receive() print(response) if response == "Access granted.": while True: display.scroll(pin) sleep(4000) display.clear()
See how it keeps trying combinations until it reaches the correct 011 PIN?
The script starts by declaring a list named digits with single character strings '0' and '1'.
digits = ['0','1']
The display.show(Image.ARROW_W) call makes the LED display point at the A button as a prompt to press it to start the process. After that, the while True: loop repeatedly checks if the A button was pressed, and does nothing else until you press it. After the A button is pressed, the display.clear() call erases the arrow so that it can start displaying the 0/1 digits without any leftover LED pixels from the arrow.
display.show(Image.ARROW_W) while True: if button_a.was_pressed(): display.clear()
These three nested loops cycle through each possible combination in pin. The script starts setting a to '0', then b to '0', and c to '0'. Then, pin = ''.join([a, b, c]) creates the string '000' by combining the three characters. The for c in digits loop isn’t done yet so it sets c to '1'. Then, pin = ''.join([a, b, c]) repeats, and this time, the result is '001'.
for a in digits: for b in digits: for c in digits: pin = ''.join([a, b, c])
Now that the for c in digits loop is finished, the for b in digits loop has to do its next iteration, setting b to '1'. Since for c in digits is below and indented, it has to repeat that loop again. This time, a is still '0', but b is now '1'. So, when c is '0', the result of pin = ''.join([a, b, c]) is '010'. On the for c in digits second repetition, it sets c to '1' and the result of pin = ''.join([a, b, c]) is '011'. It continues this way through all the possible 0/1 combinations.
A lot more happens each time through the for c loop. First, it displays the current pin combination in the terminal.
print("pin =", pin)
Then, it displays the PIN as 0/1 digits with the LED display.
for x in range(0, len(pin)): bit = int(pin[x]) brightness = bit * 9 display.set_pixel(x,4,9) for y in range(0, 4): display.set_pixel(x, y, brightness)
It also checks for the radio response from the Vault Receiver micro:bit. This loop sends repeatedly and checks for a response each time. When it does get a response, it exits the while response is None:… loop.
response = None while response is None: radio.send(pin) sleep(100) response = radio.receive()
Still inside the for c in digits… loop. Once a response has been received, it prints to the terminal. Then, if the response is "Access granted.", it means the PIN was correct. At that point, the correct PIN scrolls through the PIN Pad Transmitter micro:bit’s display repeatedly.
print(response) if response == "Access granted.": while True: display.scroll(pin)
The response string could also be "Access denied." In that case, the script just waits 4 seconds, then clears its display and lets for c in digits…loop to repeat with the next pin string in the sequence.
sleep(4000) display.clear()
If you are doing these activities by yourself, or want to make it to where you won’t have any knowledge of the PIN you are trying to crack, then modify the bank_vault_receiver script to create a random pin.
# bank_vault_receiver_random_pin # <- change from microbit import * import radio import random # <- add radio.on() radio.config(channel=7) sleep(1000) # pin = '011' # <- comment pin = '' # <- add for n in range(3): # <- add number = str(random.randint(0,1)) # <- add pin += number # <- add print("Random pin =", pin) # <- add while True: display.set_pixel(2,2,9) message = radio.receive() if message: if message == pin: radio.send("Access granted.") for n in range(4): display.show(Image.YES) sleep(1000) display.clear() sleep(200) else: radio.send("Access denied.") display.show(Image.NO) sleep(3000) display.clear()
Each time you reset the Bank Vault micro:bit, it should display a new random PIN in the terminal. (Don’t worry if your random sequence is different from what’s shown here. Yours only has a 1 in 8 chance of matching for any given reset.)
In the place of a previously decided PIN, there is now an empty string and a for loop that repeats three times—randomly choosing a 0 or 1- and then appending the pin string with that digit.
# pin = '011' # <- comment pin = '' # <- add for n in range(3): # <- add number = str(random.randint(0,1)) # <- add pin += number # <- add print("Random pin =", pin) # <- add
Up to this point, the scripts have used simple PINs with three or four binary digits. If the scripts instead use decimal digits, the increase in possible combinations improves the security by making it more time consuming to crack the PIN with brute force.
In this activity, you will use scripts that repeat what you’ve done up to now, but with decimal digits in the 0 through 5 range. Since each digit has six possible values, the number of combinations is:
6 x 6 x 6 = 63 = 216 combinations
# decimal_pin_pad_transmitter from microbit import * import radio radio.on() radio.config(channel=7) pin = '' n = 0 while True: x = len(pin) if button_a.was_pressed(): if n < 5: n += 1 if n is not 0: y = n - 1 display.set_pixel(x, y, 9) else: for y in range(0, 5): display.set_pixel(x, y, 0) n = 0 if button_b.was_pressed(): pin += str(n) n = 0 if len(pin) == 3: radio.send(pin) display.scroll(pin) pin = '' display.clear()
# decimal_bank_vault_receiver # <- change from microbit import * import radio import random radio.on() radio.config(channel=7) pin = '324' # <- change while True: display.show(Image.SQUARE_SMALL) # <- change message = radio.receive() if message: pin_entered = str(message) if pin_entered == pin: radio.send("Access granted.") for n in range(4): display.show(Image.YES) sleep(1000) display.clear() sleep(200) else: radio.send("Access denied.") display.show(Image.NO) sleep(3000) display.clear()
Again, let’s check to make sure each micro:bit is running the correct script.
Now, as you press and release the A button, LEDs will light from the top downward. No lights in a column means 0. One light means 1, and so on, up through five lights for 5.
Compared to the first binary pin_pad_transmitter script, this decimal version has the same statements, up through x = len(pin).
# decimal_pin_pad_transmitter.py from microbit import * import radio radio.on() radio.config(channel=7) pin = '' n = 0 while True: x = len(pin)
When button A is pressed to select the next digit, the script remembers that digit by increasing the value of n. It also represents 0 with no lights, 1 with the row 0 LED on, 2 with the row 1 LED on, and so on, up through 5 with the row 4 LED on. The if n < 5 block takes care of adding 1 to n and successively turning on each LED in the column. If all the lights are on and you press A again, the else block takes care of setting n to 0 and turning off all the lights.
if button_a.was_pressed(): if n < 5: n += 1 if n is not 0: y = n - 1 display.set_pixel(x, y, 9) else: for y in range(0, 5): display.set_pixel(x, y, 0) n = 0
Take a close look at if n is not 0 above. It prevents any lights from turning on when n is 0. When n is 1, it has to turn on the light in row 0. Since the row with the light on has an index of one below the value of n, y = n – 1 stores the value of the correct row index in y to turn on the correct LED.
When button B is pressed, the script has to update the pin string and reset n to 0 so that you can enter the next column. If button B is pressed to enter a digit in the third column, it also has to send the pin string to the decimal vault micro:bit.
if button_b.was_pressed(): pin += str(n) n = 0 if len(pin) == 3: radio.send(pin) display.scroll(pin) pin = '' display.clear()
In pin += str(n), the str(n) call returns a string with a single digit character that represents the value n stores. That character could be '0', '1', '2', '3', '4', or '5'. The pin += part appends that character to the pin string. When the pin string is appended with the third digit, len(pin) will return 3. At that point, the if len(pin) == 3 block sends the pin string to the Vault Receiver micro:bit, scrolls the digits across the LED display, resets pin to an empty string, and clears the display.
Aside from the comment with the script’s name, only three other lines were changed. In the original binary version, the script initialized pin to '011', then displayed a dot in the center of the LED display.
pin = '011' while True: display.set_pixel(2,2,9)
In the decimal version, all that really needed to change was the pin string—from '011' to '324'. Since there might be some confusion about whether the original binary or updated decimal version of the vault receiver script is running, the single pixel in the center of the binary version was changed to a small square in the center of the decimal version.
pin = '324' while True: display.show(Image.SQUARE_SMALL)
The brute force attack for the decimal vault is almost ready. All you have to do is add '2', '3', '4', and '5' to the digits list. The nested loops automatically go through all the items in the digits list regardless of how many items it contains. So, instead of eight combinations, the updated list will cause the script to try up to 216 combinations.
# decimal_bank_vault_crack.py # <- change from microbit import * import radio radio.on() radio.config(channel=7) # digits = ['0','1'] # <- comment (before change) digits = ['0','1','2','3','4','5'] # <- change display.show(Image.ARROW_W) while True: if button_a.was_pressed(): display.clear() for a in digits: for b in digits: for c in digits: pin = ''.join([a, b, c]) print("pin =", pin) for x in range(3): for y in range(int(pin[x])): display.set_pixel(x, y, 9) response = None while response is None: radio.send(pin) sleep(100) response = radio.receive() print(response) if response == "Access granted.": while True: display.scroll(pin) sleep(4000) display.clear()
While you are waiting for the crack script to succeed, let’s calculate how long it will take.
Since each digit counts from 0 to 5, that’s 6 possible digits: 0, 1, 2, 3, 4, and 5. After the right digit has counted through its 6 possibilities, the middle digit increases by 1, and the right digit has to start over. All told, the two right digits have 6 x 6 = 36 combinations. The third digit also has 6 possibilities, and for each of those, the right two digits must go through all their combinations. So, that’s 6 x 36 = 216.
More generally, if p = number of possible values for each digit, d = the number of digits, and c = the number of combinations, you can calculate the possible values like this:
c = pd
Let’s try it with p = 6 and d = 3. That’s:
c = 63= 216.
Now, remember that there’s a 4 second delay between each try. So the number of seconds for all combinations would be:
216 x 4 seconds = 864 seconds.
846 seconds x ( 1 minute / 60 seconds ) = 14.4 minutes.
Also, to reach 324, the decimal bank vault crack will have to go through this many combinations:
Digit-left : 3 repetitions x 36 = 108
…because the middle and right digits have to go through their cycles for each time the left digit increases by 1.
When digit-left = 3, that’s the fourth repetition, and the right digits still have some cycles.
Digit-middle needs to go through 2 more cycles, x 6 for the right digit = 12
On the middle digit’s 3rd cycle, the right digit has to count 0, 1, 2, 3, 4, which is 5 repetitions.
Total: 108 + 12 + 5 combinations = 125 combinations.
In terms of minutes, that’s:
125 combinations x 4 seconds/combination x (1 minute / 60 seconds) = 8.33 minutes.
What happens if you increase the number of digits to 4?
Answer: _______ (216 x 6 = 1296)
What’s the longest a brute force attack would take in that case?
Answer: _______ (1296 combinations x 4 seconds/combination = 5184 seconds. 5184 seconds x 1 minute / 60 seconds = 86.4 minutes. That’s over 1 hour and 26 minutes.)
Even if you add a fourth digit to the PIN, it can still be cracked in under two hours. How do modern cell phones, tablets, and teller machines deal with this problem and still keep PIN numbers short enough to be memorable? One technique they use is to only allow you a certain number of failed tries before making you wait a longer time.
Here is a terminal password example you can run on one micro:bit to understand how this process works.
# if_three_pin_fails_wait_an_hour from microbit import * sleep(1000) pin = '324' fails = 0 while True: message = input("Enter PIN: ") if message == pin: fails = 0 print("Access granted.") else: fails += 1 print("Access denied.") if fails > 2: print("Oops, 3 fails in a row!") print("Try again in an hour.") sleep(3600000) fails = 0
Assuming you didn’t have the ability to press and release the micro:bit module’s reset button after 3 incorrect tries, it would take 216 combinations x 1 hour/combination = 216 hours. You could further increase the security by having it make you wait a day before trying again, maybe after the 6th fail.
There is still a glaring vulnerability in this system! Any micro:bit listening on the same channel will still receive a correctly entered PIN. In other words, the PIN is still totally vulnerable to sniffing attacks!
The Cybersecurity: Sniffing Attacks and Defenses [3] tutorial’s Share Something Personal - Encrypted [10] activity demonstrated how even a relatively weak form of encryption can at least deter casual sniffing. However, an experienced attacker would likely examine that sniffing data and then make an educated guess that the data was encrypted with the Caesar cipher. After that, coding a brute force attack is a relatively simple matter.
In this activity, you will use brute force to crack the Caesar cipher that made sharing HAPPY, SAD, and ANGRY images (sort of) private in the Share Something Personal - Encrypted [10] activity.
# packet = caesar(-3, packet) # print("packet:", packet) # display.show(getattr(Image, packet))
for key in range(-1, -26, -1): result = caesar(key, packet) print("key:", key, "result:", result) sleep(200) print()
# radio_send_images_caesar_key_unknown.py from microbit import * import radio import random # <- add ''' Function converts plaintext to ciphertext using key ''' def caesar(key, word): alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" result = "" for letter in word: letter = letter.upper() index = ( alpha.find(letter) + key ) % 26 result = result + alpha[index] return result ''' Script starts from here... ''' radio.on() radio.config(channel=7) sleep(1000) string_list = ["HAPPY", "SAD", "ANGRY"] key = random.randint(1, 25) # <- add while True: for packet in string_list: print("packet:", packet) display.show(getattr(Image, packet)) # packet = caesar(3, packet) # <- change (before) packet = caesar(key, packet) # <- change (after) print("Send encrypted:", packet) radio.send(packet) # sleep(2500) # <- change (before) sleep(6000) # <- change (after)
# radio_receive_images_caesar_brute_force.py from microbit import * import radio ''' Function converts plaintext to ciphertext using key ''' def caesar(key, word): alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" result = "" for letter in word: letter = letter.upper() index = ( alpha.find(letter) + key ) % 26 result = result + alpha[index] return result ''' Script starts from here... ''' radio.on() radio.config(channel=7) sleep(1000) while True: packet = radio.receive() if packet: print("Receive encrypted:", packet) # packet = caesar(-3, packet) # <- comment # print("packet:", packet) # <- comment # display.show(getattr(Image, packet)) # <- comment for key in range(-1, -26, -1): # <- add result = caesar(key, packet) # <- add print("key:", key, "result:", result) # <- add sleep(200) # <- add print() # <- add
This section only explains modifications that make it use a random key and send strings at a slower rate. For info on the program before modifications, check Share Something Personal - Encrypted [10].
Adding import random makes the random methods available so that the script can use random.randint().
Adding key = random.randint(1, 25) sets the key equal to a randomly generated integer in the 1 through 25 range. This statement was placed above the while True: loop so that it gets executed before the main loop starts sending repeated encrypted messages. That way, the script sets the key value once, and then uses it with every repetition of the while True: loop.
Each time you restart the micro:bit, there is a 1 in 25 chance you will get the same key. Conversely, there is a 24 out of 25 chance that the key will be different.
In the main loop, changing packet = caesar(3, packet) to packet = caesar(key, packet) makes it so that the main loop repeatedly uses the one key value that was randomly determined before the main loop started repeating.
The sleep(2500) was changed to sleep(6000). Intentionally slowing down the broadcast rate gives the online editor enough time to take a 0.2 s break between each line printed. This will reduce the WebUSB tendency to make printing errors.
The brute force receiver removed the statements that assumed key was 3 and displayed it.
# packet = caesar(-3, packet) # <- comment # print("packet:", packet) # <- comment # display.show(getattr(Image, packet)) # <- comment
In place of those three statements, it uses a loop that decrypts and prints the decrypted packet’s result to the terminal with each of the 25 possible keys. The for key in range(-1, -26, -1) loop repeats the indented-below statements with key set to -1, then -2, -3, and so on up through -25. Each time through the loop, result = caesar(key, packet) decrypts the packet for that repetition’s key value. Then, print("key:", key, "result:", result) displays the key and decrypted string for each iteration of the loop.
for key in range(-1, -26, -1): # <- add result = caesar(key, packet) # <- add print("key:", key, "result:", result) # <- add sleep(200) # <- add print() # <- add
Lastly, sleep(200) delays before repeating the loop to give the online editor’s WebUSB connection extra time to transfer information to the terminal (since it seems to need it at the time of this writing). After all 25 loop repetitions, print() displays an empty line before the while True: loop repeats and another 25 keys and decrypted strings are displayed.
Just as a PIN is stronger with an increased number of combinations, so is the cipher. The ascii_shift cipher has more combinations.
To crack the cipher, your script will have to examine more combinations.
You might notice an upper-case HAPPY as well as a lower-case happy. The key that generates the upper-case HAPPY is the correct answer, though either might actually work for a sniffing attacker’s purposes. This is an additional weakness of the ASCII Shift cipher. Make sure not to choose a key that will simply encrypt your upper case characters as lower-case or vice-versa.
# radio_send_images_caesar_key_unknown_try_this.py from microbit import * import radio import random # <- add ''' Function converts plaintext to ciphertext using key ''' def ascii_shift(key, text): # <- change (try this) result = "" # <- chante (try this) for letter in text: # <- change (try this) ascii = ( ord(letter) + key - 32 ) % 94 + 32 # <- change (try this) result = result + chr(ascii) # <- change (try this) return result # <- change (try this) ''' Script starts from here... ''' radio.on() radio.config(channel=7) sleep(1000) string_list = ["HAPPY", "SAD", "ANGRY"] # key = random.randint(1, 25) # <- add key = random.randint(1, 93) # <- change (try this) while True: for packet in string_list: print("packet:", packet) display.show(getattr(Image, packet)) # packet = caesar(3, packet) # <- change (before) # packet = caesar(key, packet) # <- change (after) packet = ascii_shift(key, packet) # <- change (try this) print("Send encrypted:", packet) radio.send(packet) # sleep(2500) # <- change (before) sleep(6000) # <- change (after)
# radio_receive_images_caesar_brute_force_try_this.py from microbit import * import radio ''' Function converts plaintext to ciphertext using key ''' def ascii_shift(key, text): # <- change (try this) result = "" # <- chante (try this) for letter in text: # <- change (try this) ascii = ( ord(letter) + key - 32 ) % 94 + 32 # <- change (try this) result = result + chr(ascii) # <- change (try this) return result # <- change (try this) ''' Script starts from here... ''' radio.on() radio.config(channel=7) sleep(1000) while True: packet = radio.receive() if packet: print("Receive encrypted:", packet) # packet = caesar(-3, packet) # <- comment # print("packet:", packet) # <- comment # display.show(getattr(Image, packet)) # <- comment # for key in range(-1, -26, -1): # <- add for key in range(-1, -94, -1): # <- change (try this) # result = caesar(key, packet) # <- add result = ascii_shift(key, packet) # <- change (try this) print("key:", key, "result:", result) # <- add sleep(200) # <- add print() # <- add
At the end of the Cybersecurity: Encryption Intro [2] tutorial, there’s a Your Turn in the Substitution Ciphers [11] page where you created a scrambled_alphabet_cipher script. This kind of function is a much better defense against brute force attacks. Instead of testing 93 possible shifts, a brute force attack would have to try decrypting with 26! permutations of the alphabet. The term 26! is pronounced 26 factorial.
3! = 3 * 2 * 1 = 6
4! = 4 * 3 * 2 * 1 = 24
5! = 5 * 4 * 3 * 2 * 1 = 120
…
26! = 403,291,461,126,605,635,584,000,000
Wow! That would be a lot of rearrangements of the alphabet for a brute force algorithm to crack. Also, who would try to look at all those combinations to find the intelligible text?
# scrambled_alphabet_cipher from microbit import * # Scrambled alphabet cipher. def scramble(text, encrypt): alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" crypta = "PTQGMKCRSVEXADZBJFOWYNIHLU" result = "" if encrypt is False: temp = alpha alpha = crypta crypta = temp for letter in text: letter = letter.upper() index = alpha.find(letter) result = result + crypta[index] return result # The script starts executing statements from here. sleep(1000) print("Set your keyboard to CAPS LOCK.") print() while True: text = input("Enter a CAPS LOCK string: ") result = scramble(text, True) print("scrambled result =", result) result = scramble(result, False) print("unscrambled result =", result)
The scrambled alphabet cipher has two arguments, text and encrypt. In this case, the cipher accepts upper-case characters or words. The encrypt argument accepts True or False. True encrypts, and False decrypts. Inside the function, there is an alphabet, and a cryptabet. The cryptabet is a scrambled version of the alphabet. It also has an empty result string that will be used to build the encryption result.
# Scrambled alphabet cipher. def scramble(text, encrypt): alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" crypta = "PTQGMKCRSVEXADZBJFOWYNIHLU" result = ""
This step is only performed if the function is called with the encrypt argument set to False. When the function encrypts, it uses alpha and crypta as declared above. When it decrypts, alpha has to store "PTQGMKCRSVEXADZBJFOWYNIHLU" and crypta has to store "ABCDEFGHIJKLMNOPQRSTUVWXYZ". So if encrypt is False, the statements below and indented shuffle the two strings. It does so by copying the alpha string into temp, then copying the crypta into alpha. Lastly, temp — which contains the original alpha — gets copied to crypta.
if encrypt is False: temp = alpha alpha = crypta crypta = temp
The encryption loop starts by making sure the letter is upper case with letter = letter.upper(). Then, it finds the index of the letter in alpha with index = alpha.find(letter). Finally, it looks up the letter with that index in the cryptabet and appends the string result with result = result + crypta[index].
for letter in text: letter = letter.upper() index = alpha.find(letter) result = result + crypta[index] return result # The script starts executing statements from here.
The script starts executing statements here, with the usual sleep(1000) and a message to set your keyboard to CAPS LOCK.
sleep(1000) print("Set your keyboard to CAPS LOCK.") print()
Inside the main loop, an input statement prompts you to type a CAPS LOCK word and stores the result in a variable named text. Then, result = scramble(text, True) encrypts the text you typed and stores it in a variable named result, which gets printed.
while True: text = input("Enter a CAPS LOCK string: ") result = scramble(text, True) print("scrambled result =", result)
Next, result = scramble(result, False) decrypts the string, and the last print statement displays the original text you entered.
result = scramble(result, False) print("unscrambled result =", result)
It takes a while to well and truly shuffle the characters in an alphabet by hand, so why not use a script?
If you want the scramble to be repeatable, you can add a statement like random.seed(10) anywhere above while n < size: For each different seed, you will get a different, but repeatable, result. Without a manually entered seed value, the micro:bit uses the system timer to get a more random seed to determine the sequence.
# cryptabet_autogenerate from microbit import * import random sleep(1000) alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" size = len(alpha) print("size: ", size) print("alpha:", alpha) sleep(1500) crypta = "" n = 0 while n < size: sleep(50) r = random.randint(0, size-1) c = alpha[r] print("r:", r, ", c:", c) if crypta.find(c) == -1: crypta = crypta + c n = n + 1 print("n:", n, ", crypta:", crypta) sleep(1000) print("crypta:", crypta) sleep(100) print()
The cryptabet_auto_generator script starts with alpha, which is set to the upper-case alphabet for the sake of example. It repeatedly generates random numbers in the 0…25 range, and then grabs the character at that index from alpha. Then, it checks if that character is already in crypta. If yes, then it discards the character and tries again. If no, then it appends the character to crypta and starts adding the next character.
Sleep statements were added at various points to give the online editor’s terminal enough time to keep up with the stream of text data.
The script uses the length of the alpha string to build the crypta string. So there would be no problem using a custom alphabet, like alpha = "vLR{}:,’01234 56789", for example. That particular alphabet would support dictionaries from the Terminal Control - Go Wireless [12] activity.
Now that you know how to better encrypt communications, you can strengthen apps from previous activities against brute force attacks.
A scrambled alphabet substitution cipher can make the app from Share Something Personal - Encrypted [13] activity much more difficult to crack with a brute force attack.
You can even use cryptabet_autogenerate to generate a custom cryptabet. If you just use the one that’s in the scrambled_alphabet_cipher script, your fellow students might have a very easy time cracking your cipher with a simple guess and test!
This cipher does not need to just have upper-case alphabet letters. You could, for example, make alpha and crypta strings that contain the characters for the keyboard-controlled cyber:bot’s dictionary. The alpha and crypta strings might look might look like this:
alpha = "vLR{}:,’01234 56789" crypta = "v9L:3405’R286, 7{}1"
With nineteen characters, there are 19! = 121,645,100,408,832,000 different ways to arrange the characters in this string.
For an example of the steps to follow to add encryption to your data applications, try the Encrypt Your App Data [15] activity in the Cybersecurity: Sniffing Attacks and Defenses [3] tutorial.
Links
[1] https://python.microbit.org/v/2
[2] https://learn.parallax.com/tutorials/robot/cyberbot/cybersecurity-encryption-intro
[3] https://learn.parallax.com/tutorials/robot/cyberbot/cybersecurity-sniffing-attacks-and-defenses
[4] https://commons.wikimedia.org/wiki/File:Bletchley_Park_Bombe4.jpg
[5] https://creativecommons.org/licenses/by-sa/3.0
[6] https://learn.parallax.com/tutorials/language/python/cybersecurity-radio-basics/texting-terminals
[7] https://learn.parallax.com/tutorials/robot/cyberbot/cybersecurity-radio-basics/make-microbit-radios-send-and-receive/send-radio
[8] https://learn.parallax.com/tutorials/robot/cyberbot/cybersecurity-encryption-intro/encrypt-letters-caesar-cipher-script/how
[9] https://learn.parallax.com/node/2284
[10] https://learn.parallax.com/tutorials/language/python/cybersecurity-sniffing-attacks-and-defenses/share-something-personal-1
[11] https://learn.parallax.com/tutorials/robot/cyberbot/cybersecurity-encryption-intro/ascii-and-other-simple-ciphers
[12] http://learn.parallax.com/tutorials/robot/cyberbot/cybersecurity-navigation-control-keyboard/terminal-control-%E2%80%94-go-wireless
[13] https://learn.parallax.com/node/2282
[14] https://learn.parallax.com/tutorials/robot/cyberbot/cybersecurity-navigation-control-keyboard/terminal-control-%E2%80%94-go-wireless
[15] https://learn.parallax.com/tutorials/robot/cyberbot/cybersecurity-sniffing-attacks-and-defenses/encrypt-your-app-data