Last active
October 26, 2018 02:50
-
-
Save petertseng/73f9c539e4ecea12cf96dcad3c5a0438 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # http://www.mountainvistasoft.com/chaocipher/ActualChaocipher/Chaocipher-Revealed-Algorithm.pdf | |
| # suggested improvements (IV) | |
| # https://pthree.org/2015/07/09/the-chaocipher-with-playing-cards/ | |
| # reasonable allegation that one alphabet is irrelevant, so strength is only 26! | |
| # https://prgomez.com/scrabble-cipher/ | |
| def permute_left(left, idx) | |
| rotated = left.chars.rotate(idx) | |
| rotated[1..13] = rotated[2..13] << rotated[1] | |
| rotated.join | |
| end | |
| def permute_right(right, idx) | |
| rotated = right.chars.rotate(idx + 1) | |
| rotated[2..13] = rotated[3..13] << rotated[2] | |
| rotated.join | |
| end | |
| def cipher(msg, whiches, left, right) | |
| l = left.dup | |
| r = right.dup | |
| [ | |
| msg.each_char.zip(whiches.each_char).map { |(c, which)| | |
| raise "Invalid #{which}" unless 'LR'.include?(which) | |
| ps, cs = which == ?L ? [l, r] : [r, l] | |
| was_lower = c == c.downcase | |
| idx = ps.index(c.upcase) | |
| next c unless idx | |
| x = cs[idx] | |
| (was_lower ? x.downcase : x).tap { | |
| l = permute_left(l, idx) | |
| r = permute_right(r, idx) | |
| } | |
| }.join, | |
| [l, r] | |
| ] | |
| end | |
| def encipher(msg, left, right) | |
| cipher(msg, ?R * msg.size, left, right).first | |
| end | |
| def decipher(msg, left, right) | |
| cipher(msg, ?L * msg.size, left, right).first | |
| end | |
| def key(phrase, pattern) | |
| assert_eq(phrase.size, pattern.size, 'phrase/pattern size match') | |
| a_to_z = (?A..?Z).to_a.join.freeze | |
| cipher(phrase, pattern, a_to_z, a_to_z).last | |
| end | |
| def assert_eq(want, got, desc) | |
| raise "#{desc}: Want #{want}, got #{got}" if got != want | |
| end | |
| assert_eq('PFJRIGTWOBNYQEHXUCZVAMDSLK', permute_left(l = 'HXUCZVAMDSLKPEFJRIGTWOBNYQ'.freeze, l.index(?P)), 'rotate left') | |
| assert_eq('VZGJRIHWXUMCPKTLNBQDEOYSFA', permute_right(r = 'PTLNBQDEOYSFAVZKGJRIHWXUMC'.freeze, r.index(?A)), 'rotate right') | |
| assert_eq(c = 'OAHQHCNYNXTSZJRRHJBYHQKSOUJY'.freeze, encipher(p = 'WELLDONEISBETTERTHANWELLSAID'.freeze, l, r), 'encipher uppercase') | |
| assert_eq(c.downcase.freeze, encipher(p.downcase.freeze, l, r), 'encipher lowercase') | |
| assert_eq(c.capitalize.freeze, encipher(p.capitalize.freeze, l, r), 'encipher mixed') | |
| assert_eq(p, decipher(c, l, r), 'decipher uppercase') | |
| assert_eq(p.downcase.freeze, decipher(c.downcase, l, r), 'decipher lowercase') | |
| assert_eq(p.capitalize.freeze, decipher(c.capitalize, l, r), 'decipher mixed') | |
| assert_eq(ks = ['BFVGUHWJKNCPEDQRSTIXYLMOZA', 'CMOPRTUVJXAYZNBQDSEFGHLWIK'].map(&:freeze).freeze, key('THINKTHINK', rl = 'RLLRLLRRLR'), 'key uppercase') | |
| assert_eq(ks, key('thinkthink', rl), 'key lowercase') | |
| def test_iv(iv_len, trials) | |
| # Does a shorter IV reduce the variation in the first letter? | |
| a_to_z = (?A..?Z).to_a.freeze | |
| l, r = key('thinkthink', 'RLLRLLRRLR') | |
| l.freeze | |
| r.freeze | |
| trials.times.map { | |
| iv = Array.new(iv_len) { a_to_z.sample }.join | |
| encipher(iv + 'A', l, r)[iv_len] | |
| }.group_by(&:itself).transform_values(&:size) | |
| end | |
| #p test_iv(20, 1000).sort_by(&:last).each { |k, v| puts "#{k}: #{v}" } | |
| def test_iv2(iv_len) | |
| # How much does a long IV reveal about the key coming before it? | |
| # To see this, vary the letter before the IV, and show the results. | |
| a_to_z = (?A..?Z).to_a.freeze | |
| iv = Array.new(iv_len) { a_to_z.sample }.join | |
| a_to_z.map { |c| [c, key('thinkthink' + c + iv, 'RLLRLLRRLR' + ?R * (1 + iv_len))] }.to_h | |
| end | |
| #test_iv2(20).each { |k, v| puts "#{k}: #{v}" } | |
| def chunk(s, n) | |
| s.each_slice(n).map(&:join).join(' ') | |
| end | |
| IV_LEN = 20 | |
| PREFIX_LEN = 20 | |
| def encipher_iv_prefix(msg, left, right) | |
| a_to_z = (?A..?Z).to_a.freeze | |
| # An IV to... prevent same plaintext from mapping to same ciphertext? | |
| # Doesn't the random prefix do this too? IV might not be necessary... | |
| # I'm not convinced, so I'm not going to use this. | |
| iv = Array.new(IV_LEN) { a_to_z.sample } | |
| # A random prefix, to make it harder for a known-plaintext to result in key discovery. | |
| prefix = chunk(Array.new(PREFIX_LEN) { a_to_z.sample }, 5) | |
| chunk(iv, 5) + ' ' + encipher(iv.join + prefix + ' ' + msg, left, right)[iv.size..-1] | |
| end | |
| def decipher_iv_prefix(msg, left, right) | |
| iv_chunk_len = IV_LEN * 6 / 5 | |
| iv, msg = [msg[0...iv_chunk_len], msg[iv_chunk_len..-1]] | |
| l, r = cipher(iv, ?R * iv.size, left, right).last | |
| prefix_chunk_len = PREFIX_LEN * 6 / 5 | |
| decipher(msg, l, r)[prefix_chunk_len..-1] | |
| end | |
| l, r = key('thinkthink', 'RLLRLLRRLR') | |
| c = encipher_iv_prefix(p = 'my best message ever', l, r) | |
| assert_eq(p, decipher_iv_prefix(c, l, r), 'decipher with IV and random prefix') | |
| def encipher_with_prefix(msg, left, right) | |
| a_to_z = (?A..?Z).to_a.freeze | |
| # A random prefix, to make it harder for a known-plaintext to result in key discovery. | |
| prefix = chunk(Array.new(PREFIX_LEN) { a_to_z.sample }, 5) | |
| encipher(prefix + ' ' + msg, left, right) | |
| end | |
| def decipher_with_prefix(msg, left, right) | |
| prefix_chunk_len = PREFIX_LEN * 6 / 5 | |
| decipher(msg, left, right)[prefix_chunk_len..-1] | |
| end | |
| l, r = key('thinkthink', 'RLLRLLRRLR') | |
| c = encipher_with_prefix(p = 'my best message ever', l, r) | |
| assert_eq(p, decipher_with_prefix(c, l, r), 'decipher with random prefix') | |
| decipher_mode = ARGV.delete('-d') | |
| if ARGV.size <= 1 | |
| puts "usage: #{$PROGRAM_NAME} [-d] [-e] <key> [LRLRLR] <message to encrypt>" | |
| exit 9001 | |
| end | |
| keyphrase = ARGV.shift | |
| keywhich = /^[LR]$/.match?(ARGV[0]) ? ARGV.shift : ?R * keyphrase.size | |
| l, r = key(keyphrase, keywhich) | |
| msg = ARGV.join(' ') | |
| puts decipher_mode ? decipher_with_prefix(msg, l, r) : encipher_with_prefix(msg, l, r) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment