Skip to content

Instantly share code, notes, and snippets.

@petertseng
Last active October 26, 2018 02:50
Show Gist options
  • Select an option

  • Save petertseng/73f9c539e4ecea12cf96dcad3c5a0438 to your computer and use it in GitHub Desktop.

Select an option

Save petertseng/73f9c539e4ecea12cf96dcad3c5a0438 to your computer and use it in GitHub Desktop.
# 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