mirror of
https://github.com/Smaug123/dotnet-classical-ciphers
synced 2025-10-05 04:28:42 +00:00
119 lines
4.0 KiB
Forth
119 lines
4.0 KiB
Forth
namespace CipherSuite
|
|
|
|
#if DEBUG
|
|
#else
|
|
#nowarn "9"
|
|
#endif
|
|
|
|
[<RequireQualifiedAccess>]
|
|
module Monoalphabetic =
|
|
|
|
/// The hill climber is allowed to accept a worse solution sometimes
|
|
/// (in the name of escaping local optima). This is a table of probabilities
|
|
/// for how much worse the solution is allowed to be; for example, there is a
|
|
/// 5 in 20 chance that we don't accept any worse solution, and a 2 in 20 chance
|
|
/// we accept a solution that is 8-or-more worse.
|
|
/// I have completely lost the source of these numbers, and it appears that
|
|
/// by sheer coincidence the scale I chose for the built-in fitness function
|
|
/// happens to be very compatible with these perturbations.
|
|
let private perturbations =
|
|
[|
|
|
0
|
|
0
|
|
0
|
|
0
|
|
0
|
|
1
|
|
1
|
|
1
|
|
1
|
|
1
|
|
2
|
|
2
|
|
2
|
|
3
|
|
4
|
|
4
|
|
5
|
|
6
|
|
8
|
|
15
|
|
|]
|
|
|> Array.map float
|
|
|
|
let private decrypt (key : int[]) (ciphertext : int[]) (output : int[]) : unit =
|
|
for i = 0 to ciphertext.Length - 1 do
|
|
output.[i] <- key.[ciphertext.[i]]
|
|
|
|
let inline private swap (arr : int[]) (p1 : int) (p2 : int) : unit =
|
|
let temp = arr.[p1]
|
|
arr.[p1] <- arr.[p2]
|
|
arr.[p2] <- temp
|
|
|
|
let rec private backToString (text : int[]) : string =
|
|
text |> Array.map ((+) (int 'a') >> char) |> System.String
|
|
|
|
// I *think* this may originally be an algorithm described in
|
|
// http://web.archive.org/web/20110210123730/http://web.mac.com/mikejcowan/Ciphers/Churn_Algorithm.html
|
|
// But it was over a decade ago and I've simply forgotten the source.
|
|
let crack (rand : System.Random) (keysLimit : int) (ciphertext : string) : int[] * string * float =
|
|
let ciphertext = ciphertext.ToUpperInvariant ()
|
|
#if DEBUG
|
|
let fitness = Fitness.fromEmbedded ()
|
|
#else
|
|
let arr = Fitness.allocate ()
|
|
use ptr = fixed arr
|
|
let fitness = Fitness.fromEmbedded ptr
|
|
#endif
|
|
let plaintext =
|
|
Array.init
|
|
ciphertext.Length
|
|
(fun i ->
|
|
let output = int ciphertext.[i] - int 'A'
|
|
|
|
if output < 0 || output > 25 then
|
|
failwith "non-alphabetic character"
|
|
|
|
output
|
|
)
|
|
|
|
let ciphertext = Array.copy plaintext
|
|
let currentKey = [| 0..25 |]
|
|
let bestKey = Array.copy currentKey
|
|
decrypt currentKey ciphertext plaintext
|
|
let mutable currentFitness = Fitness.get' plaintext fitness
|
|
let mutable bestFitness = currentFitness
|
|
let mutable keyCount = 0
|
|
|
|
while keyCount < keysLimit do
|
|
let swap1 = rand.Next (0, 26)
|
|
let swap2 = rand.Next (0, 25)
|
|
let swap2 = if swap2 < swap1 then swap2 else swap2 + 1
|
|
|
|
swap currentKey swap1 swap2
|
|
|
|
// We could do better here, because the plaintext changes
|
|
// only in well-defined ways from loop to loop.
|
|
// I'm lazy.
|
|
decrypt currentKey ciphertext plaintext
|
|
|
|
let newFitness = Fitness.get' plaintext fitness
|
|
|
|
if newFitness > bestFitness then
|
|
bestFitness <- newFitness
|
|
currentFitness <- newFitness
|
|
Array.blit currentKey 0 bestKey 0 currentKey.Length
|
|
elif newFitness > currentFitness - perturbations.[rand.Next (0, perturbations.Length)] then
|
|
// This guess wasn't the best we've had, but it's good enough when we
|
|
// add in a bit of jitter to get us out of local optima.
|
|
currentFitness <- newFitness
|
|
else
|
|
// Revert this guess, it wasn't sufficiently high fitness
|
|
swap currentKey swap1 swap2
|
|
|
|
keyCount <- keyCount + 1
|
|
|
|
decrypt bestKey ciphertext plaintext
|
|
let plaintext = backToString plaintext
|
|
bestKey, plaintext, bestFitness
|