diff --git a/submissions/challenge1/jan_opalski/README.md b/submissions/challenge1/jan_opalski/README.md new file mode 100644 index 0000000..67e3315 --- /dev/null +++ b/submissions/challenge1/jan_opalski/README.md @@ -0,0 +1,16 @@ +# Language +The code was written in Go. These instructions are based on assumption that user trying to run this solution has Go on their machine. +# Running the solution +- enter the directory +- `go run .` (alternatively: `go build` and then `./challenge1`) +## Flags +By default all three solutions will be launched. If you want to see results of execution of only chosen tasks, you can achieve that by passing a subset of flags `-easy`, `-medium`, `-hard` respectively (passing all 3 is effectively the same as passing none of these). + +Solution of medium task (implementation of Mersenne Twister RNG) uses a default seed of 19650218 (taken from [English Wikipedia](https://en.wikipedia.org/wiki/Mersenne_Twister)). It can be changed to arbitrary value (assuming it can be converted to unsigned 32-bit integer) using flag `-seed `. + +Example command with flags: `./challenge1 -medium -seed 42`. This will launch only the Mersenne Twister implementation with a starting seed of 42. + +# Done tasks +- Easy: [PKCS#7 padding validation](https://cryptopals.com/sets/2/challenges/15) +- Medium: [Implement the MT19937 Mersenne Twister RNG](https://cryptopals.com/sets/3/challenges/21) +- Hard: [Recover the key from CBC with IV=Key](https://cryptopals.com/sets/4/challenges/27) \ No newline at end of file diff --git a/submissions/challenge1/jan_opalski/go.mod b/submissions/challenge1/jan_opalski/go.mod new file mode 100644 index 0000000..930f847 --- /dev/null +++ b/submissions/challenge1/jan_opalski/go.mod @@ -0,0 +1,3 @@ +module challenge1 + +go 1.24.4 diff --git a/submissions/challenge1/jan_opalski/main.go b/submissions/challenge1/jan_opalski/main.go new file mode 100644 index 0000000..1dd90a1 --- /dev/null +++ b/submissions/challenge1/jan_opalski/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "challenge1/solutions" + "flag" +) + +func main() { + easyFlag := flag.Bool("easy", false, "launch solution of easy task") + mediumFlag := flag.Bool("medium", false, "launch solution of medium task") + hardFlag := flag.Bool("hard", false, "launch solution of hard task") + mediumSeedFlag := flag.Int("seed", 19650218, "seed for medium task") + flag.Parse() + all := false + if !(*easyFlag || *mediumFlag || *hardFlag) { + flag.Usage() + all = true + } + if *easyFlag || all { + solutions.Easy() + } + if *mediumFlag || all { + solutions.Medium(uint32(*mediumSeedFlag)) + } + if *hardFlag || all { + solutions.Hard() + } +} diff --git a/submissions/challenge1/jan_opalski/solutions/easy.go b/submissions/challenge1/jan_opalski/solutions/easy.go new file mode 100644 index 0000000..dbeba30 --- /dev/null +++ b/submissions/challenge1/jan_opalski/solutions/easy.go @@ -0,0 +1,47 @@ +package solutions + +import ( + "errors" + "fmt" +) + +// https://cryptopals.com/sets/2/challenges/15 +func Easy() { + fmt.Println("=============================\n\t EASY (15)\n=============================") + str1 := "ICE ICE BABY\x04\x04\x04\x04" + str2 := "ICE ICE BABY\x05\x05\x05\x05" + str3 := "ICE ICE BABY\x01\x02\x03\x04" + test_stripping(str1) + test_stripping(str2) + test_stripping(str3) + str4 := "\x02\x02" + test_stripping(str4) +} + +func strip_padding(msg string) (string, error) { + if len(msg) == 0 { + return msg, nil + } + padding_len := msg[len(msg)-1] + first_pad := len(msg) - int(padding_len) + + if first_pad < 0 { + return "", errors.New("padding invalid") + } + for i := first_pad; i < len(msg); i++ { + if msg[i] != padding_len { + return "", errors.New("padding invalid") + } + } + return msg[:first_pad], nil +} + +func test_stripping(msg string) { + fmt.Printf("Testing stripping for %q\n", msg) + unpadded, err := strip_padding(msg) + if err != nil { + fmt.Println("Got error:", err) + } else { + fmt.Println("Correctly unpadded message", unpadded) + } +} diff --git a/submissions/challenge1/jan_opalski/solutions/hard.go b/submissions/challenge1/jan_opalski/solutions/hard.go new file mode 100644 index 0000000..442c1ef --- /dev/null +++ b/submissions/challenge1/jan_opalski/solutions/hard.go @@ -0,0 +1,116 @@ +package solutions + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "slices" +) + +const blocklen = 16 +const blockcount = 3 + +// https://cryptopals.com/sets/4/challenges/27 +func Hard() { + fmt.Println("=============================\n\t HARD (27)\n=============================") + // create a 128-bit key, assume sender and receiver know it while attacker doesn't + key := sender_receiver_setKey() + // SENDER + message := sender_setupMessage() + encrypted := sender_encryptMessage(message, key) + + // ATTACKER + modified := attacker_modifyMessage(encrypted) + + // RECEIVER + err := receiver_decryptMessage(modified, key) + if err == nil { + fmt.Println("Receiver didn't raise error <- attack failed") + return + } + + // ATTACKER + decrypted, err := hex.DecodeString(err.Error()) + if err != nil || len(decrypted) == 0 { + fmt.Println("Didn't get decrypted \"plaintext\" of positive length <- attack failed") + return + } + att_key := attacker_guessKey(decrypted) + + // verification + if !slices.Equal(key, att_key) { + fmt.Println("Attacker got the key wrong") + } else { + fmt.Println("Attacker got the key right") + } +} + +func sender_receiver_setKey() []byte { + key := make([]byte, blocklen) + _, _ = io.ReadFull(rand.Reader, key) + fmt.Println("Key:\t\t\t", hex.EncodeToString(key)) + return key +} + +func sender_setupMessage() []byte { + message := []byte("This is a very important and very secret message") + if len(message) != blockcount*blocklen { + panic("Message not padded to be exactly 3-blocks long") + } + return message +} + +// (P1, P2, P3) -> (C1, C2, C3) +func sender_encryptMessage(msg []byte, key []byte) []byte { + block, _ := aes.NewCipher(key) + // create CBC cipher which has unsafe behaviour of reusing key as iv + encrypter := cipher.NewCBCEncrypter(block, key) + encrypted := make([]byte, len(msg)) + encrypter.CryptBlocks(encrypted, msg) + fmt.Println("Encrypted by sender:\t", hex.EncodeToString(encrypted)) + return encrypted +} + +// (C1, C2, C3) -> (C1, 0, C1) +func attacker_modifyMessage(msg []byte) []byte { + modified := make([]byte, len(msg)) + copy(modified[:blocklen], msg[:blocklen]) + copy(modified[2*blocklen:3*blocklen], msg[:blocklen]) + fmt.Println("Modified by attacker:\t", hex.EncodeToString(modified)) + return modified +} + +// The assumption in case of processing error (decrypted plaintext containing invalid ASCII value) +// receiver returns some kind of error message which contains the decrypted plaintext. +// Here for the sake of simplicity receiver_ans error message is strictly either +// - hex string of decrypted "plaintext" if there was an invalid ASCII value (almost surely there'll be) +// - nothing if there was no invalid ASCII value +func receiver_decryptMessage(encrypted []byte, key []byte) error { + block, _ := aes.NewCipher(key) + decrypter := cipher.NewCBCDecrypter(block, key) + decrypted := make([]byte, len(encrypted)) + decrypter.CryptBlocks(decrypted, encrypted) + for _, char := range decrypted { + if char > 0x7F { + // Invalid ASCII, return error answer + return errors.New(hex.EncodeToString(decrypted)) + } + } + return nil // Kind of "OK" response +} + +// decrypted "plaintext" block1 ^ block3 should be equal to iv aka key (it's the same here) +// will work always as long as we get "plaintext" from receiver +func attacker_guessKey(receiver_ans []byte) []byte { + fmt.Println("Receiver thorws error:\t", hex.EncodeToString(receiver_ans)) + key := receiver_ans[:blocklen] + for i := range blocklen { + key[i] ^= receiver_ans[2*blocklen+i] + } + fmt.Println("Attacker guesses key:\t", hex.EncodeToString(key)) + return key +} diff --git a/submissions/challenge1/jan_opalski/solutions/medium.go b/submissions/challenge1/jan_opalski/solutions/medium.go new file mode 100644 index 0000000..7571fe1 --- /dev/null +++ b/submissions/challenge1/jan_opalski/solutions/medium.go @@ -0,0 +1,53 @@ +package solutions + +import ( + "fmt" +) + +// https://cryptopals.com/sets/3/challenges/21 +func Medium(seed uint32) { + fmt.Println("=============================\n\t MEDIUM (21)\n=============================") + rng := newMt(seed) + fmt.Println("First 5 integers generated for seed", seed) + for range 5 { + fmt.Println(rng.extractNumber()) + } +} + +type my_mtrng struct { + mt [624]uint32 + index int +} + +// used pseudocode from https://pl.wikipedia.org/wiki/Mersenne_Twister +func newMt(seed uint32) my_mtrng { + ans := my_mtrng{} + ans.mt[0] = seed + for i := uint32(1); i < 624; i++ { + ans.mt[i] = 0x6c078965*(ans.mt[i-1]^(ans.mt[i-1]>>30)) + i + } + return ans +} + +func (rng *my_mtrng) extractNumber() uint32 { + if rng.index == 0 { + rng.generateNumbers() + } + y := rng.mt[rng.index] + y ^= (y >> 11) + y ^= ((y << 7) & 0x9d2c5680) + y ^= ((y << 15) & 0xefc60000) + y ^= (y >> 18) + rng.index = (rng.index + 1) % 624 + return y +} + +func (rng *my_mtrng) generateNumbers() { + for i := range 624 { + y := rng.mt[i]&(1<<31) | rng.mt[(i+1)%624]&(^uint32(1<<31)) + rng.mt[i] = rng.mt[(i+397)%624] ^ (y >> 1) + if y%2 == 1 { + rng.mt[i] ^= 0x9908b0df + } + } +}