Kotlin - Simple Cipher WIP
This commit is contained in:
parent
620cafefff
commit
c70f0c823a
8 changed files with 361 additions and 0 deletions
93
kotlin/simple-cipher/README.md
Normal file
93
kotlin/simple-cipher/README.md
Normal file
|
@ -0,0 +1,93 @@
|
|||
# Simple Cipher
|
||||
|
||||
Implement a simple shift cipher like Caesar and a more secure substitution cipher.
|
||||
|
||||
## Step 1
|
||||
|
||||
"If he had anything confidential to say, he wrote it in cipher, that is,
|
||||
by so changing the order of the letters of the alphabet, that not a word
|
||||
could be made out. If anyone wishes to decipher these, and get at their
|
||||
meaning, he must substitute the fourth letter of the alphabet, namely D,
|
||||
for A, and so with the others."
|
||||
—Suetonius, Life of Julius Caesar
|
||||
|
||||
Ciphers are very straight-forward algorithms that allow us to render
|
||||
text less readable while still allowing easy deciphering. They are
|
||||
vulnerable to many forms of cryptoanalysis, but we are lucky that
|
||||
generally our little sisters are not cryptoanalysts.
|
||||
|
||||
The Caesar Cipher was used for some messages from Julius Caesar that
|
||||
were sent afield. Now Caesar knew that the cipher wasn't very good, but
|
||||
he had one ally in that respect: almost nobody could read well. So even
|
||||
being a couple letters off was sufficient so that people couldn't
|
||||
recognize the few words that they did know.
|
||||
|
||||
Your task is to create a simple shift cipher like the Caesar Cipher.
|
||||
This image is a great example of the Caesar Cipher:
|
||||
|
||||
![Caesar Cipher][1]
|
||||
|
||||
For example:
|
||||
|
||||
Giving "iamapandabear" as input to the encode function returns the cipher "ldpdsdqgdehdu". Obscure enough to keep our message secret in transit.
|
||||
|
||||
When "ldpdsdqgdehdu" is put into the decode function it would return
|
||||
the original "iamapandabear" letting your friend read your original
|
||||
message.
|
||||
|
||||
## Step 2
|
||||
|
||||
Shift ciphers are no fun though when your kid sister figures it out. Try
|
||||
amending the code to allow us to specify a key and use that for the
|
||||
shift distance. This is called a substitution cipher.
|
||||
|
||||
Here's an example:
|
||||
|
||||
Given the key "aaaaaaaaaaaaaaaaaa", encoding the string "iamapandabear"
|
||||
would return the original "iamapandabear".
|
||||
|
||||
Given the key "ddddddddddddddddd", encoding our string "iamapandabear"
|
||||
would return the obscured "lpdsdqgdehdu"
|
||||
|
||||
In the example above, we've set a = 0 for the key value. So when the
|
||||
plaintext is added to the key, we end up with the same message coming
|
||||
out. So "aaaa" is not an ideal key. But if we set the key to "dddd", we
|
||||
would get the same thing as the Caesar Cipher.
|
||||
|
||||
## Step 3
|
||||
|
||||
The weakest link in any cipher is the human being. Let's make your
|
||||
substitution cipher a little more fault tolerant by providing a source
|
||||
of randomness and ensuring that the key is not composed of numbers or
|
||||
capital letters.
|
||||
|
||||
If someone doesn't submit a key at all, generate a truly random key of
|
||||
at least 100 characters in length, accessible via Cipher#key (the #
|
||||
syntax means instance variable)
|
||||
|
||||
If the key submitted has capital letters or numbers, throw an
|
||||
ArgumentError with a message to that effect.
|
||||
|
||||
## Extensions
|
||||
|
||||
Shift ciphers work by making the text slightly odd, but are vulnerable
|
||||
to frequency analysis. Substitution ciphers help that, but are still
|
||||
very vulnerable when the key is short or if spaces are preserved. Later
|
||||
on you'll see one solution to this problem in the exercise
|
||||
"crypto-square".
|
||||
|
||||
If you want to go farther in this field, the questions begin to be about
|
||||
how we can exchange keys in a secure way. Take a look at [Diffie-Hellman
|
||||
on Wikipedia][dh] for one of the first implementations of this scheme.
|
||||
|
||||
[1]: https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Caesar_cipher_left_shift_of_3.svg/320px-Caesar_cipher_left_shift_of_3.svg.png
|
||||
[dh]: http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange
|
||||
|
||||
|
||||
|
||||
## Source
|
||||
|
||||
Substitution Cipher at Wikipedia [http://en.wikipedia.org/wiki/Substitution_cipher](http://en.wikipedia.org/wiki/Substitution_cipher)
|
||||
|
||||
## Submitting Incomplete Solutions
|
||||
It's possible to submit an incomplete solution so you can see how others have completed the exercise.
|
28
kotlin/simple-cipher/build.gradle
Normal file
28
kotlin/simple-cipher/build.gradle
Normal file
|
@ -0,0 +1,28 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.1.1'
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
|
||||
}
|
||||
test {
|
||||
testLogging {
|
||||
exceptionFormat = 'full'
|
||||
events = ["passed", "failed", "skipped"]
|
||||
}
|
||||
}
|
13
kotlin/simple-cipher/simple-cipher.iml
Normal file
13
kotlin/simple-cipher/simple-cipher.iml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module external.linked.project.id="simple-cipher" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/out" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
46
kotlin/simple-cipher/src/main/kotlin/Cipher.kt
Normal file
46
kotlin/simple-cipher/src/main/kotlin/Cipher.kt
Normal file
|
@ -0,0 +1,46 @@
|
|||
import java.util.*
|
||||
|
||||
class Cipher() {
|
||||
var key: String
|
||||
|
||||
constructor(key: String) : this() {
|
||||
require(key.isNotEmpty())
|
||||
require(key.all { it.isLowerCase() })
|
||||
this.key = key
|
||||
}
|
||||
|
||||
init {
|
||||
val rand = Random()
|
||||
val stringBuilder = StringBuilder()
|
||||
|
||||
for (i in 0..99) {
|
||||
stringBuilder.append('a' + rand.nextInt(26))
|
||||
}
|
||||
|
||||
key = String(stringBuilder)
|
||||
|
||||
}
|
||||
|
||||
fun encode(s: String): String {
|
||||
require(s.length <= key.length)
|
||||
val stringBuilder = StringBuilder()
|
||||
|
||||
for ((index, i) in s.withIndex()){
|
||||
stringBuilder.append(i + (key[index].toInt() - 'a'.toInt()))
|
||||
}
|
||||
|
||||
return String(stringBuilder)
|
||||
}
|
||||
|
||||
fun decode(s: String): String {
|
||||
require(s.length <= key.length)
|
||||
val stringBuilder = StringBuilder()
|
||||
|
||||
for ((index, i) in s.withIndex()){
|
||||
stringBuilder.append(i - (key[index].toInt() - 'a'.toInt()))
|
||||
}
|
||||
|
||||
return String(stringBuilder)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import org.junit.Test
|
||||
import org.junit.Ignore
|
||||
|
||||
class IncorrectKeyCipherTest {
|
||||
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun cipherThrowsWithAllCapsKey() {
|
||||
Cipher("ABCDEF")
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun cipherThrowsWithAnyCapsKey() {
|
||||
Cipher("abcdEFg")
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun cipherThrowsWithNumericKey() {
|
||||
Cipher("12345")
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun cipherThrowsWithAnyNumericKey() {
|
||||
Cipher("abcd345ef")
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun cipherThrowsWithEmptyKey() {
|
||||
Cipher("")
|
||||
}
|
||||
}
|
57
kotlin/simple-cipher/src/test/kotlin/RandomKeyCipherTest.kt
Normal file
57
kotlin/simple-cipher/src/test/kotlin/RandomKeyCipherTest.kt
Normal file
|
@ -0,0 +1,57 @@
|
|||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.Ignore
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class RandomKeyCipherTest {
|
||||
|
||||
private lateinit var cipher: Cipher
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
cipher = Cipher()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun cipherKeyIsMadeOfLetters() {
|
||||
assertTrue(cipher.key.matches(Regex("[a-z]+")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultCipherKeyIs100Characters() {
|
||||
assertEquals(100, cipher.key.length)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cipherKeysAreRandomlyGenerated() {
|
||||
assertNotEquals(Cipher().key, cipher.key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Here we take advantage of the fact that plaintext of "aaa..." doesn't output the key. This is a critical problem
|
||||
* with shift ciphers, some characters will always output the key verbatim.
|
||||
*/
|
||||
@Test
|
||||
fun cipherCanEncode() {
|
||||
val expectedOutput = cipher.key.substring(0, 10)
|
||||
|
||||
assertEquals(expectedOutput, cipher.encode("aaaaaaaaaa"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cipherCanDecode() {
|
||||
val expectedOutput = "aaaaaaaaaa"
|
||||
|
||||
assertEquals(expectedOutput, cipher.decode(cipher.key.substring(0, 10)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cipherIsReversible() {
|
||||
val plainText = "abcdefghij"
|
||||
|
||||
assertEquals(plainText, cipher.decode(cipher.encode(plainText)))
|
||||
}
|
||||
}
|
11
kotlin/simple-cipher/src/test/kotlin/SimpleCipherTest.kt
Normal file
11
kotlin/simple-cipher/src/test/kotlin/SimpleCipherTest.kt
Normal file
|
@ -0,0 +1,11 @@
|
|||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Suite
|
||||
|
||||
@RunWith(Suite::class)
|
||||
@Suite.SuiteClasses(
|
||||
RandomKeyCipherTest::class,
|
||||
IncorrectKeyCipherTest::class,
|
||||
SubstitutionCipherTest::class
|
||||
)
|
||||
class SimpleCipherTest {
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.Ignore
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class SubstitutionCipherTest {
|
||||
|
||||
private val KEY = "abcdefghij"
|
||||
private lateinit var cipher: Cipher
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
this.cipher = Cipher(KEY)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun cipherKeepsTheSubmittedKey() {
|
||||
assertEquals(KEY, cipher.key)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun cipherCanEncodeWithGivenKey() {
|
||||
val expectedOutput = "abcdefghij"
|
||||
|
||||
assertEquals(expectedOutput, cipher.encode("aaaaaaaaaa"))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun cipherCanDecodeWithGivenKey() {
|
||||
val expectedOutput = "aaaaaaaaaa"
|
||||
|
||||
assertEquals(expectedOutput, cipher.decode("abcdefghij"))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun cipherIsReversibleGivenKey() {
|
||||
val plainText = "abcdefghij"
|
||||
|
||||
assertEquals(plainText, cipher.decode(cipher.encode("abcdefghij")))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun cipherCanDoubleShiftEncode() {
|
||||
val plainText = "iamapandabear"
|
||||
val expectedOutput = "qayaeaagaciai"
|
||||
|
||||
assertEquals(expectedOutput, Cipher(plainText).encode(plainText))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun cipherCanWrapEncode() {
|
||||
val expectedOutput = "zabcdefghi"
|
||||
|
||||
assertEquals(expectedOutput, cipher.encode("zzzzzzzzzz"))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun cipherCanEncodeMessageThatIsShorterThanTheKey() {
|
||||
val expectedOutput = "abcde"
|
||||
|
||||
assertEquals(expectedOutput, cipher.encode("aaaaa"))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun cipherCanDecodeMessageThatIsShorterThanTheKey() {
|
||||
val expectedOutput = "aaaaa"
|
||||
|
||||
assertEquals(expectedOutput, cipher.decode("abcde"))
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue