Portfolio: Vigenère Cipher
View raw: vigenere.rb
#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
#
# Copyright by Scott Severance
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# This is a library to implement the Vigenère Cipher
# <https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher>.
#
# Compatibility note:
# -------------------
# If you want to use a random key, *and* you don't supply a seed, this library
# depends on a UNIX-like system (such as Linux). If you don't need that
# particular feature, you should be able to use any system with a Ruby 1.8 or
# 1.9 interpreter. This library is untested with Ruby 2.0.
require 'enumerator'
# Exception raised if the key is set before the text.
class NoTextError < StandardError
end
# This class handles encoding and decoding text using the
# Vigenère Cipher.
class Vigenere
# For convenience, you can set the options in the constructor. All
# values are optional (though they need to be specified eventually).
#
# Arguments:
# key: The cipher key
# text: The text to be encoded or decoded
# chars_per_group: If non-nil, output will be grouped into groups of
# this many characters.
#
# When setting the text and the key, this class will automatically
# convert its input to uppercase and strip all non-alphabetic
# characters.
def initialize(key = nil, text = nil, chars_per_group = nil)
@letter_set = %w[A B C D E F G H I J K L M N O P Q R S T U V W X Y Z]
@table, @letter_indices = make_tabula_recta
@chars_per_group = chars_per_group
self.text = text unless text.nil?
self.key = key unless key.nil?
@seeded = nil
end
# Reader methods for various instance variables.
attr_reader :chars_per_group, :cipher_text, :letter_set, :plain_text, :seed, :table
# Set the number of characters per output group. It is an error to set
# this to 0. Set to nil to disable grouping.
def chars_per_group=(n)
raise ArgumentError, "Can't have 0 characters per group; set to nil to disable" if n == 0
@chars_per_group = n
end
# Decode the stored text using the stored key. Of course, both must be
# set prior to calling this method.
def decode
transform :decode
end
# Encode the stored text using the stored key. Of course, both must be
# set prior to calling this method.
def encode
transform :encode
end
# Reads the specified file and sets its contents as the text.
def file=(filename)
File.open(filename) do |f|
self.text = f.read
end
self.text
end
# Provides sensible output on inspect and when running in irb.
def inspect
"<Vigenere: @text=%s, @key=%s, @seed=%s>" % [
(@text) ? @text.inspect : 'nil',
(@key) ? @key.inspect : 'nil',
(@seed) ? @seed.inspect : 'nil',
]
end
# Returns the current key.
def key
add_spaces(@key, chars_per_group)
end
# Sets the key. Raises NoTextError unless the text is set first.
# Automatically converts the key to uppercase and discards
# non-alphabetic characters.
def key=(key)
begin
txt_len = @text.split('').length # A bug in Ruby 1.8 sometimes causes
# String#length to return an incorrect value
rescue NoMethodError
raise NoTextError, "You must set the text before setting a key."
end
@original_key = key
key = normalize_text(key).split('')
until key.length >= txt_len
key += key
end
@key = key[0...txt_len].join('')
@key
end
# Sets the alphabet to use. Takes an array of uppercase (if
# applicable) letters. Note: normalize_text can only auto-uppercase
# the letters of the English alphabet. That means that if you use a
# non-English alphabet, you'll have to manually convert your text and
# key to uppercase.
def letter_set=(array)
@letter_set = array
@table, @letter_indices = make_tabula_recta
end
# Makes the tabula_recta--the cipher table. Returns an array of the
# table itself and a hash mapping letters to indices.
def make_tabula_recta
letters = @letter_set.dup
table = []
letters.length.times do
table.push letters.dup
letters.push letters.shift
end
indices = {}
letters = letters.each_with_index { |letter, i| indices[letter] = i }
[table, indices]
end
# Generates a pseudorandom key the same length as the text. Sets and
# returns the key.
#
# If a seed has been set via the seed= method, then this method
# generates a repeatable pseudorandom number based on the seed. If the
# seed has not been set, or has been explicitly set to nil, then this
# method generates the most random number possible on the system.
#
# Compatibility note: In cases where @seed is nil (meaning a seed has not been
# set via the seed= method), this implementation depends on the underlying
# system having a 'dd' command and a '/dev/random'. This most likely means
# that this case will only work on a UNIX-like system. It has only been tested
# on Linux.
def random_key
text_length = @text.split('').length # See explanation in method key=.
if @seed
key = []
text_length.times do
key.push(@table[0][rand(@letter_set.length)])
end
else
cmd = "dd if=/dev/random bs=1 count=#{text_length}"
key = `#{cmd} 2>/dev/null`.chomp.unpack("C#{text_length}")
key.collect! { |i| @table[0][i % @letter_set.length] }
end
self.key = key.join ''
end
# To use a seeded random number for a random key (so that the key can
# be replayed, call this method with an Integer. To use a more random
# key, call it with nil.
def seed=(value)
srand(value) if value.kind_of? Integer
@seed = value
end
# Returns the current text.
def text
add_spaces(@text, @chars_per_group)
end
# Sets the text. Automatically converts text to uppercase and discards
# non-alphabetic characters.
def text=(text)
@text = normalize_text text
self.key= @original_key if @original_key
end
# Handles conversion to a string. Returns the text. Text is in
# whichever state it was last converted to.
def to_s
text
end
# The following methods are private.
private
# Adds spaces to str every chars_per_group characters.
def add_spaces(str, chars_per_group = nil)
return str if chars_per_group.nil?
str = str.split ''
out = []
str.each_slice(chars_per_group) do |group|
group.push ' '
out.push group.join('')
end
out.join('').strip
end
# Converts text to uppercase and discards non-alphabetic characters.
def normalize_text(text)
letters = text.upcase.split ''
letters.delete_if { |x| ! @table[0].include?(x) }
letters.join ''
end
# Performs the actual encoding or decoding. The argument direction is
# one of :encode or :decode.
def transform(direction)
text_letters = @text.split ''
key_letters = @key.split ''
transformed = []
case direction
when :encode
text_letters.each_with_index do |letter, i|
index1 = @letter_indices.fetch(key_letters.fetch(i))
index2 = @letter_indices.fetch(letter)
transformed.push @table.fetch(index1).fetch(index2)
end
@plain_text = @text
@cipher_text = add_spaces(transformed.join(''), @chars_per_group)
@text = normalize_text @cipher_text
return @cipher_text
when :decode
text_letters.each_with_index do |letter, i|
index = @letter_indices.fetch(letter) - @letter_indices.fetch(key_letters.fetch(i))
transformed.push @table[0].fetch(index)
end
@plain_text = add_spaces(transformed.join(''), @chars_per_group)
@cipher_text = self.text
@text = normalize_text @plain_text
return @plain_text
else
raise ArgumentError, "direction must be either :encode or :decode"
end
end
end
# Testing code. Only executed if this file is executed directly. If this
# file is require'd, this code doesn't run.
#
# To run the testing code:
# ./vigenere.rb text key [chars-per-group]
if $0 == __FILE__
raise "Must supply at least 2 command line arguments" if ARGV.length < 2
a = Vigenere.new
a.text = ARGV[0]
a.key = ARGV[1]
a.chars_per_group = ARGV[2].to_i if ARGV.length >= 3
puts "Using supplied key:"
puts " Plain text : #{a}"
puts " Supplied key : #{a.key}"
puts " Encoded text : #{a.encode}"
puts " Decoded text : #{a.decode}"
a.seed = 7629384752639857236945827346952837652938752392938742837462598
a.random_key
puts "\nUsing random (seeded) key:"
puts " Plain text : #{a}"
puts " Random key : #{a.key}"
puts " Encoded text : #{a.encode}"
puts " Decoded text : #{a.decode}"
end