https://github.com/bitspook/cryptopals
Solving cryptopals challenges using Rust
https://github.com/bitspook/cryptopals
Last synced: about 1 year ago
JSON representation
Solving cryptopals challenges using Rust
- Host: GitHub
- URL: https://github.com/bitspook/cryptopals
- Owner: bitspook
- Created: 2021-12-09T12:23:44.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2022-02-07T10:30:41.000Z (over 4 years ago)
- Last Synced: 2025-02-08T06:41:44.254Z (over 1 year ago)
- Language: TypeScript
- Size: 1.04 MB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: readme.org
Awesome Lists containing this project
README
#+title: Cryptopals
#+author: Charanjit Singh
#+HTML_HEAD:
#+HTML_HEAD:
#+OPTIONS: html-style:nil num:nil creator:comment
#+STARTUP: hideblocks
#+PROPERTY: header-args:obs :tangle nil
This page is me solving [[https://cryptopals.com/sets/1/challenges/6][cryptopals challenges]] with Rust, and taking
notes. UI widgets accompanying the rust snippets are running the wasm
compiled version of the snippet.
#+begin_quote
Please note that although I am publishing this publicly, I am learning
a bunch of things at the same time here. So don't take my word for
anything; be vigilant about mistakes and misstatements.
#+end_quote
* Set 1
** Challenge 1: Convert Hex to Base64
:PROPERTIES:
:header-args: :tangle src/set1/challenge1.rs :comments link
:ID: 92fceb57-a247-4011-a440-088db62ac4ee
:END:
#+begin_src rust :exports none
use anyhow::{Result, Error};
use wasm_bindgen::prelude::*;
#+end_src
First challenge is straightforward enough, we are given a HEX encoded
string, and we have to encode it to base64 instead.
I decided to not go down the rabbit hole here and use the libraries
which handle encoding/decoding to/from hex/base64.
#+BEGIN_SRC rust
pub fn hex_to_b64(input: &str) -> Result {
let hex_str = hex::decode(input).map_err(Error::from)?;
let out = base64::encode(hex_str);
Ok(out)
}
#+END_SRC
I am using [[https://github.com/dtolnay/anyhow][anyhow]] library to return =anyhow::Result= here; because it
is convenient. I also use it in my (relatively) bigger rust
applications.
This solves the challenge without fuss.
#+begin_export html
Some glue code for browser.
#+end_export
#+attr_html: :class hex-to-b64-glue
#+begin_src rust
#[wasm_bindgen]
pub fn hex_to_b64_web(input: &str) -> String {
hex_to_b64(input).unwrap()
}
#+end_src
#+ATTR_OBS: :module c1
#+BEGIN_SRC obs :tangle nil
c1_cipher = "49276d206b696c6c696e6720796f757220627261696e206c696b65206120706f69736f6e6f7573206d757368726f6f6d";
c1_solution = cryptopals.hex_to_b64_web(c1_cipher);
#+END_SRC
Entering the test string given in the [[https://cryptopals.com/sets/1/challenges/1][challenge]] produce the expected
output.
But let's go a step further and write a test! One of the things I like
about rust is how easy it makes to write tests. We can drop the
following snippet in the same file and =cargo test= will run it. The
lack of friction makes for a great developer experience.
#+BEGIN_SRC rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn s1e1_hex_to_b64() {
let input = "49276d206b696c6c696e6720796f757220627261696e206c696b65206120706f69736f6e6f7573206d757368726f6f6d";
let output = "SSdtIGtpbGxpbmcgeW91ciBicmFpbiBsaWtlIGEgcG9pc29ub3VzIG11c2hyb29t";
assert_eq!(hex_to_b64(input).unwrap(), output);
}
}
#+END_SRC
** Challenge 2: Fixed XOR
:PROPERTIES:
:header-args: :tangle src/set1/challenge2.rs :comments link
:END:
Next challenge is not particularly challenging either. We are given
two strings of equal length, and we have to perform XOR bitwise
operation on them. We can simply use rust's =^= operator, which does
exactly that.
But, this time let's start with writing tests first.
#+begin_src rust :exports none
use wasm_bindgen::prelude::*;
#+end_src
#+BEGIN_SRC rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_xor() {
let b1 = hex::decode("1c0111001f010100061a024b53535009181c").unwrap();
let b2 = hex::decode("686974207468652062756c6c277320657965").unwrap();
let expected = "746865206b696420646f6e277420706c6179";
assert_eq!(xor(&b1, &b2), hex::decode(expected).unwrap());
}
#[test]
fn test_hexor() {
assert_eq!(
hexor(
"1c0111001f010100061a024b53535009181c",
"686974207468652062756c6c277320657965"
),
"746865206b696420646f6e277420706c6179"
)
}
}
#+END_SRC
We are given two hex encoded strings, which we'll first decode. In the
first challenge, it was recommended that we should work directly with
bytes when we can; instead of any encoded form of strings. So we'll
write our =xor= function to accept references to byte arrays (=u8= is
one byte), and return a new byte array with every byte of one buffer
XOR'd against that of second.
But I am not sure how to use =&[u8]= in Javascript, and I am not
willing to put time in this right now (I am side-questing a lot
already), so we'll also create a helper which can work directly with
strings. We'll call it =hexor= to put emphasis on the fact that its
input strings are hex encoded.
#+BEGIN_SRC rust
pub fn xor(b1: &[u8], b2: &[u8]) -> Vec {
let mut result: Vec = vec![];
for i in 0..b1.len() {
result.push(b1[i] ^ b2[i]);
}
result
}
#[wasm_bindgen]
pub fn hexor(s1: &str, s2: &str) -> String {
let b1 = hex::decode(s1).unwrap();
let b2 = hex::decode(s2).unwrap();
hex::encode(xor(&b1, &b2))
}
#+END_SRC
#+begin_export html
#+end_export
** Challenge 3: Single-byte XOR cipher
:PROPERTIES:
:header-args: :tangle src/set1/challenge3.rs :comments link
:END:
#+begin_export html
const hexedCipher = "1b37373331363f78151b7f2b783431333d78397828372d363c78373e783a393b3736";
#+end_export
This is the challenge that put me on the quest of solving
cryptopals. I encountered a version of this exercise while trying to
do the [[https://overthewire.org/wargames/natas/][natas wargame]]; and got side-quested. This is also the first
exercise where we get a taste of cryptography.
We are given a hex-encoded string which is ciphered with a *single
character*, and we have to decrypt it. Neat!
"Single character" implies the key is an ASCII character, which means
a single byte represented with numbers from 0 to 255. We can simply
brute-force our way through this one, try every key from 0 to 255, and
see which one decrypts the cipher. The latter is the hard part.
How can we tell if decryption was successful?
1. We can just look at the decrypted result and see if it is garbage
or not.
2. Figure out how to code #1
Let's do both. Let's first write a function which when given a key
(i.e a single byte) and a cipher string, can undo the XOR applied on
them. Which is of course, XOR. We can just reuse the =xor= function we
wrote in previous challenge, but that one expects two byte-arrays of
equal length.
Let's write a function which takes a byte-array and repeat it to given
length.
#+BEGIN_SRC rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_repeat_till() {
let input = "key".as_bytes();
let expected = "keykeykeykeykeyke".as_bytes();
assert_eq!(repeat_till(input, 17), expected);
}
}
#+END_SRC
Try and see if you can make this test pass.
#+begin_export html
Reveal Solution
#+end_export
#+ATTR_HTML: :class repeat-till
#+BEGIN_SRC rust
pub fn repeat_till(input: &[u8], length: usize) -> Vec {
let mut result = vec![];
for i in 0..length {
result.push(input[i % input.len()]);
}
result
}
#+END_SRC
We can now combine these two functions to try brute-force decrypting
the given cipher.
#+begin_export html
Let's quickly write some glue code to
do so right in the browser.
#+end_export
#+ATTR_HTML: :class fixed-key-xor
#+BEGIN_SRC rust
#[wasm_bindgen]
pub fn fixed_key_xor(hexedCipher: &str, key: u8) -> String {
let cipher = hex::decode(hexedCipher).unwrap();
let repeated_key = repeat_till(&[key], cipher.len());
let result = xor(&cipher, &repeated_key);
std::str::from_utf8(&result)
.expect("Invalid utf8 chars in string")
.to_string()
}
#+END_SRC
#+begin_export html
#+end_export
Since many ASCII characters are invisible, our little function accepts
the ASCII code directly, and treat it as a byte. So instead of using
=A= as key, we have to use =65=. Play around with it and see if you
can find the key which decrypts the actual message. [[https://en.wikipedia.org/wiki/ASCII#Printable_characters][Printable ASCII
codes]] fall between 32 and 126.
Now for the next step, let's try and think how we can automate
detecting if decryption was successful.
A successful decryption would mean obtaining the plain text English
sentence. So the test we want to write is for identifying whether a
given string is a legible English sentence. An obvious approach would
be to check if the words in the string are valid English words or not,
i.e check if most of the words are also present in English dictionary.
But we can do better. The cryptopals site gives a hint: *ETAOIN
SHRDLU*. Searching "ETAOIN SHRDLU cryptograpy" results in finding that
it is the approximate order of 12 most commonly used English letters,
mentioned on [[https://en.wikipedia.org/wiki/Frequency_analysis][Wikipedia article on Frequency analysis]]. There are a lot
of references of using frequency analysis to decrypt simple ciphers,
so I think it's safe to go this way.
Different from the wikipedia article, instead of doing the frequency
analysis on ciphertext, we are going to do it on the plain-text we
obtain after a decryption attempt. Decrypted text whose letter
frequency matches [[https://en.wikipedia.org/wiki/Letter_frequency][that of normal English]] best will the winner.
First let's write a function to determine letter-frequency of a given
string. We'll start by writing a test:
#+BEGIN_SRC rust
#[cfg(test)]
mod lf_tests {
use super::*;
#[test]
fn test_letter_frequency() {
let input = "aaaaccddee";
let lf = letter_frequency(input);
assert_eq!(lf.get(&'a'), Some(&0.4));
assert_eq!(lf.get(&'c'), Some(&0.2));
assert_eq!(lf.get(&'d'), Some(&0.2));
assert_eq!(lf.get(&'e'), Some(&0.2));
}
}
#+END_SRC
#+begin_export html
And then make it pass.
#+end_export
#+ATTR_HTML: :class letter-frequency
#+BEGIN_SRC rust
pub fn letter_frequency(input: &str) -> HashMap {
let mut lf = HashMap::new();
for c in input.chars() {
,*lf.entry(c.to_ascii_lowercase()).or_default() += 1.0;
}
for v in lf.values_mut() {
,*v /= input.len() as f64;
}
lf
}
#+END_SRC
We want to reach a score of some kind, which can allow us to compare
decryption results of two attempts. Let's go for [[https://en.wikipedia.org/wiki/Mean_squared_error][Mean Squared Error]]. I
am not good with statistics, but as per what I understand from
Wikipedia, MSE should fit the bill for us.
We'll start with writing tests. We'll call our function
=letter_frequency_error= to indicate that it is calculating how wrong
the letter frequency of the given string is when compared with the
[[https://en.wikipedia.org/wiki/Letter_frequency][standard]].
#+BEGIN_SRC rust
#[cfg(test)]
mod lfe_tests {
use super::letter_frequency_error;
#[test]
fn test_letter_frequency_error() {
let input = "She sells sea shells at the sea shore. Shells are blue and they are white, ocean is blue and it is bright.";
let error_till_2dec = (letter_frequency_error(input) * 100.0).trunc() / 100.0;
assert_eq!(error_till_2dec, 0.26);
}
}
#+END_SRC
#+begin_export html
Reveal Solution
#+end_export
#+ATTR_HTML: :class letter-frequency-error
#+BEGIN_SRC rust
#[wasm_bindgen]
pub fn letter_frequency_error(input: &str) -> f64 {
let standard_freq = HashMap::from([
('a', 0.08167),
('b', 0.01492),
('c', 0.20782),
('d', 0.04253),
('e', 0.12702),
('f', 0.02228),
('g', 0.02015),
('h', 0.06094),
('i', 0.06966),
('j', 0.00153),
('k', 0.00772),
('l', 0.04025),
('m', 0.02406),
('n', 0.06749),
('o', 0.07507),
('p', 0.01929),
('q', 0.00095),
('r', 0.05987),
('s', 0.06327),
('t', 0.09056),
('u', 0.02758),
('v', 0.00978),
('w', 0.02360),
('x', 0.00150),
('y', 0.01974),
('z', 0.00074),
]);
let letter_freq = letter_frequency(input);
let mut freq_sum: f64 = 0.0;
for (letter, s_freq) in &standard_freq {
let freq = letter_freq.get(letter).unwrap_or(&0.0);
let freq_diff = *freq - *s_freq;
freq_sum += freq_diff * freq_diff;
}
(freq_sum / letter_freq.len() as f64) * 100.0
}
#+END_SRC
Looks like we have all the pieces. Time to connect them and see if our
approach produces any good results.
#+BEGIN_SRC rust
#[derive(Serialize, Deserialize)]
pub struct Crack {
key: String,
plain_text: String,
}
pub fn crack_single_key_xor_cipher(hexedCipher: &str) -> Crack {
let mut solution: (u8, String, f64) = (0, "".to_string(), 99.0);
for key in 1..255 {
let cipher = hex::decode(hexedCipher).unwrap();
let repeated_key = repeat_till(&[key], cipher.len());
let result = xor(&cipher, &repeated_key);
if let Ok(result) = std::str::from_utf8(&result) {
let lfe = letter_frequency_error(result);
if lfe < solution.2 {
solution = (key, result.to_string(), lfe);
}
}
}
Crack {
key: solution.0.to_string(),
plain_text: solution.1,
}
}
#+END_SRC
To make things a bit more readable, and for feel-good reasons, we've
created a =Struct= to hold our possible solution. Our approach is
simple:
1. For every =key= from 1 to 255, i.e ASCII range
- =xor= the cipher with =key=
- Try converting it to utf8 =plain_text=
- Find =letter_frequency_error= of =plain_text=
2. =plain_text= with smallest =letter_frequency_error= is the solution
But is it? Let's play around with this function and see if it can
crack the cipher given in cryptopals challenge.
#+begin_export html
Some glue code for web.
#+end_export
#+ATTR_HTML: :class crack-single-key-xor-cipher-glue
#+BEGIN_SRC rust
#[wasm_bindgen]
pub fn crack_single_key_xor_cipher_web(hexedCipher: &str) -> JsValue {
JsValue::from_serde(&crack_single_key_xor_cipher(hexedCipher)).unwrap()
}
#+END_SRC
# Local Variables:
# org-html-htmlize-font-prefix: "hljs-"
# org-html-htmlize-output-type: css
# End: