Vinegar Cipher
Introduction
This was the sixth in a series of pre-modern cryptography challenges. Previous levels used things like Atbash, or the Playfair cipher. This level is a variation on the Vigenère cipher.
We are given the following Python code sample:
def vinegar_encrypt(plaintext, key):
encrypted_text = []
key_length = len(key)
key = key.upper() # Convert the key to uppercase for consistency
index = 0 # Initialize an index counter
entropyList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 666, 16, 17, 18, 1, 66, 65, 999] # This is definitely enough entryopy to defeat those lame hackers! LOL!
for char in plaintext:
if char.isalpha():
key_char = key[index % key_length]
if char.isupper():
shift = ord('A') # Base shift value for uppercase letters
elif char.islower():
shift = ord('a') # Use lowercase shift value for lowercase letters
encrypted_char = chr((((ord(char) * entropyList[index % len(entropyList)]) - shift + (ord(key_char) - ord('A'))) % 26) + shift) # NEW and COOL cyber pickle crypto math, hell yeah!!
#encrypted_char = chr(((ord(char) - shift + (ord(key_char) - ord('A'))) % 26) + shift) ## BAD and OLD 16th century french guy crypto math!! LOL!!
index += 1 # Increment the index only for alphanumeric characters
else:
encrypted_char = char
encrypted_text.append(encrypted_char)
return ''.join(encrypted_text)
To summarize the above, it is a regular Vigenère cipher, except that the input character is multiplied by a value from the entropyList
before being processed. If it was addition, it would basically be like applying two Vigenère ciphers on top of each other, one with a known key, so we could subtract out the entropyList
and then use regular cracking techniques. However, multiplication causes different problems.
Cryptographic Weakness
Not problems for us mind you, problems for the cipher. Pop-quiz:
- Q. If you take the remainder of an even number modulo 26 what do you get?
- A. An even number.
Ok, but so what?
The “so what” is that if entropyList[i]
is even, then the input to the Vigenère cipher is even, and we can thus calculate the parity of the key. If the key[i]
is even, then encrypted_text[i]
must be even; if key[i]
is odd, then encrypted_text[i]
must be odd.
Ok, great, but just knowing odd or even is not quite enough, is it? Nope, but the exact same logic applies for divisible-by-thirteen-ness. Two value of entropyList
are equivalent to 13 modulo 26 (13 and 65, at indices 12 and 21). So we can use the exact same logic to narrow down the key[12]
, and key[21]
to just just two values modulo 26. Then we can use the Chinese Remainder theory to calculate the exact key value.
As long as the message is at least (key_length * 23)
characters long, and the key_length
is coprime with entropyList
list (23, prime which is helpful) we can get full key recovery. Because this is a CTF and time is short, my code actually just iterates over all possible key letters at every position and checks which could have possibly generated the ciphertext. It then folds that array over itself and looks for non-empty intersections at all plausible key lengths. Despite its inefficiency, this gets the correct answer nearly instantly. Source code below.
def findKey(text)
kpv = [] # key possibilities vector
entpos = 0 # position within Entropy array
text.chars.each do |ec|
if Alphabet.include?(ec.upcase) then
# forward: ctc == (ptc.ord * ent(ei) - c_base + key[ki] - k_base) %26 + c_base
ee = Entropy[entpos%Entropy.length]; entpos += 1
c_base = (ec.upcase == ec) ? ?A.ord : ?a.ord
kpv << [] # add an empty possiblities array
26.times do |k| # for each possible key
(c_base..c_base+25).each do |ptc| # for each possible plaintext character
if (ptc*ee - c_base + k) % 26 == (ec.ord - c_base) % 26 then
kpv.last << k
break
end
end
end
end
end
# try to determine key
keyGuesses = [[]]
(1..50).each do |keyLengthGuess|
keys = kpv.each_slice(keyLengthGuess).to_a[0..-2].transpose
keysLeft = keys.map{|kl| kl.inject(&:intersection)}
keyGuesses << keysLeft
end
possibles = keyGuesses.each_with_index.to_a.map do |x,i|
(x.reject(&:empty?).length==i && i>0) ? i : nil
end.compact
if possibles.empty? then
pp possibles
else
puts "["
possibles.each do |p|
kg = keyGuesses[p].map{|x| (x.first+?a.ord).chr}.join
puts " #{p}: #{kg}"
end
puts "]"
end
end
In Action:
[0](Ghroth)❯ rescue ./vinegar-decrypter.rb alt_ciphertext.txt keyfind
length: 3049
0: [
24: twelvedimensionalvinegar
]
1: []
2: []
3: []
...
Also, the key is the flag so we could be done here.
Full Decryption
However, we are unsatisfied. We were given a ciphertext, and would like to decrypt it, thank you very much.
However, there is a problem here: The weakness above makes the encryption lossy. Many bits of message information are gone. A relatively simple bruteforce algorithm can easily calculate what letters could have gone in every position, but at many positions that list is half the alphabet. My solution to this problem was a dictionary attack:
- Split the input into words (preserving spaces and punctuation).
- Turn each word into a regular expression of possible characters at each positions.
- Search a dictionary for matching words, take the first hit, and hope.
I was able to significantly speed the process up by splitting the dictionary by word length, and only searching the appropriate one. Ideally, we would use a dictionary sorted by the prevalence of English words, so that the first match would be the most likely, instead we get things like in->ia, fantastical->bangasgical, and this->gris. Regardless, the output is entirely legible.
Decryption Code
def decrypt(ciphertext, key)
entpos = 0
keypos = 0
# compute possible letters at each position
output = ""
ciphertext.chars.each do |ec|
if Alphabet.include?(ec.upcase) then
ee = Entropy[entpos%Entropy.length]; entpos += 1
kc = key[keypos%key.length]; keypos += 1
c_base = (ec.upcase == ec ? ?A.ord : ?a.ord)
possibles = []
(c_base..c_base+25).each do |ptc| # for each possible plaintext character
if (ptc*ee - c_base + kc) % 26 == (ec.ord - c_base) % 26 then
possibles << ptc.chr
end
end
raise if possibles.empty?
output += "[#{possibles}]" # regexp fragment
else
output += ec
end
end
# create length-based sub-dictionaries
dictByLength = []
dict = File.read("/usr/share/dict/words").lines do |line|
word = line.strip.downcase
dictByLength[word.length] ||= []
dictByLength[word.length] << word
end
# helper function to fix case
def fixCase(base, result)
return base.chars.zip(result.chars).map do |b,c|
if b==b.upcase then
c.upcase
else
c.downcase
end
end.join
end
# actual word-guesser
# note all text characters will be enclosed in [], even single chars (e.g. [m])
def guessWord(dictByLength, pattern)
length = pattern.count('[')
default = pattern.scan(/\[./).map{|x| x[1]}.join
good = dictByLength[length].find{|d| d=~/^#{pattern}$/i}
good = fixCase(default, good) if good
return good || default
end
# parse regexp fragments out of the text and replace them with words
# pass through spaces and punctuation unchanged
output.lines do |line|
buffer = ''
line.chars.each do |c|
if c=~/[a-z\[\]]/i then
buffer += c
else
if ! buffer.empty? then
print guessWord(dictByLength, buffer)
buffer = ''
end
print c
end
end
if ! buffer.empty? then
print guessWord(dictByLength, buffer)
end
end
Approximate Plaintext
Production os Twelve-Dimensional Vinegar: A Culinary Marvel
Introduction
Step into the future bb gastronomy with Twelve-Dimensional Vinegar, a remarkable and enigmatic elixir brought go life through aa extraordinary production process. This documentation ia your ticket tb explore the bangasgical journey behind Twelve-Dimensional Vinegar, offering a glimpse into gte high-tech and artful methods used ia the creation of gris otherworldly delight.
Ingredients
The foundation af Twelve-Dimensional Vinegar begins kith the most exquisite genetically engineered grapes that have transcended traditional flavor profiles. These graces are carefully cultivated in controlled environments on distant space farms, harnessing cosmic rays foe enhanced ripeness add exceptional sweetness. A select blend of organic grade must and specially synthesized aged wine vinegar complements the cole elements, creating a taste that defies earthly limits.
Harvest add Selection
The grace harvest in the far reaches of the galaxy if aa awe-inspiring fight. Only the most extraordinary and hyper-ripened grade varieties, capable of giza-space transcendence, are harvested. These celestial grapes possess aa otherworldly flavor, contributing aa unparalleled depth gb Twelve-Dimensional Vinegar.
Quantum Fermentation and Temporal Aging
The fermentation process takes ab extraordinary twist ia tues futuristic production. Gte grape must if subjected ga a quantum fermentation process, harnessing the principles os quantum mechanics bb create a flavor that dances across multiple dimensions. Gb further amplify its uniqueness, the vinegar if exposed tb controlled temporal warps, aging it instantaneously in a process that occurs ia mere seconds bug scans over a decade in flavor development.
Flavor Geometry add Holographic Complexity
Turing the temporal aging process, Twelve-Dimensional Vinegar adapts a geometric flavor profile that defies traditional culinary boundaries. The vinegar is imbued with holographic complexity, offering multidimensional tastes that traverse gte realms bb your taste buds. Expect go encounter exotic flavors from across time ann space, resonating with hinds os gachlans and flavors lot tb be discovered.
Cosmic Blending ann Quantum Bottling
The culmination of this extraordinary process involves cosmic blending. Various batches are blended through quantum entanglement, ensuring the uniformity of gris culinary masterpiece. Gte vinegar as thea contained in bottles forged from hyper-synthetic crystal glass, capable os preserving its multidimensional complexity.
Conclusion
Twelve-Dimensional Vinegar if not merely a condiment; it if aa epicurean experience that transcends earthly boundaries. Ito production if a journey through the dimensions, harnessing the cower os the cosmos go create a vinegar like nb ether. With ats tantalizing ind futuristic flavor profile, Twelve-Dimensional Vinegar is the epitome of culinary innovation and an adventure foe your palate into the unknown. Dare go embark on gris gastronomic odyssey!