Zaszyfrowana pocztówka od Gynvaela

W połowie czerwca ze skrzynki pocztowej wyjąłem taką pocztówkę:

PocztówkaPocztówka

Odcyforwanie jej znaczenia zajęło mi ponad miesiąc, a bezpośrednio nad nią spędziłem kilka-kilkanaście godzin. Biorąc pod uwagę ostateczną trudność rozwiązania jest to trochę wstyd, ale jak to się mawia:

hindsight is 20/20

Transkrypcja

Gynvael ma lepszy styl pisma niż ja, więc problemów z przepisaniem nie było:

TXT = <<HEX
    50 6F 7A 64 72 6F
    77 69 65 6E 69 61
    20 [[ 7A 64 22 70
    68 6A 68 62 6B 73
    64 6E 6B 62 2E 43
    7F 72 73 61 7A 7A
    36 2C 31 39 5D 62
    72 ]]
HEX

Przygotowanie

Pierwsze wykonanym krokiem było sprawdzenie, jak te dane wyglądają jeśli potraktowć je jako ASCII (a raczej UTF-8):

TXT.split.map{ |n| n.to_i(16) }.select{ |x| x > 0 }.map(&:chr).join
Pozdrowienia zd\"phjhbksdnkb.C\x7Frsazz6,19]br

Od razu widać, że część nie została w ogóle zaszyfrowana. Początkowo może wydawać się, że “Pozdrowienia z” jest niezaszyfrowane, ale obecność [[ sugeruje, że jest to wyłącznie “Pozdrowienia ” (“z” jest pierwszym znakiem wewnątrz podwójnych kwadratowych nawiasów).

W tym momencie tekst można podzielić na część jawną i zaszyfrowaną:

CLEAR = TXT.split[0...13].map { |n| n.to_i(16) }
SECRET = TXT.split[14...-1].map { |n| n.to_i(16) }

Próby nieudane

Zanim odnalazłem rozwiązanie, podjąłem wiele prób. Poniżej opiszę część z nich. Kolejność rozwiązań nie została zachowana, ponieważ nie prowadziłem notatek, a było to już kilka tygodni temu.

A może EBCDIC?

require 'iconv'
 
p Iconv.conv('ASCII', 'EBCDIC-US', SECRET.map(&:chr).join)

Niestety nie.

gyn.rb:110:in `conv': "d\"phjhbksdnkb.C\x7F"... (Iconv::IllegalSequence)
        from gyn.rb:110:in `<main>'

Sprawdzenie wartości bajtów faktycznie pokazało, że nie ma tu żadnej sensownej wiadomości. Na osłodę dodam, że okazałem się nieświadomym pomysłodawcą 6. polskiej misji:

<Gynvael> 01:11 <KrzaQ> okej, ebcdic to tez nie jest
<Gynvael> sam mi podsunales pomysł
<Gynvael> :DDD

Jest to też smutny komentarz na temat tego, ile zajęło mi czasu opisanie tego rozwiązania.

A co tam robi to 7F16?

7F16, czyli binarnie 01111111 znajduje się mniej więcej w środku części zaszyfrowanej: poprzedzone jest 16 znakami, a za nim jest kolejne 12. Może jakiś xor, mnożenie, suma, and, or?

p1, p2 = SECRET[0..15], SECRET[17..-1]
p p1
p p2
 
[122, 100, 34, 112, 104, 106, 104, 98, 107, 115, 100, 110, 107, 98, 46, 67]
[114, 115, 97, 122, 122, 54, 44, 49, 57, 93, 98, 114]

xor:

p p1.zip(p2)
    .reject{ |_, v| v == nil }
    .map{ |k,v| k ^ v }
    .map(&:chr)
 
["\b", "\x17", "C", "\n", "\x12", "\\", "D", "S", "R", ".", "\x06", "\x1C"]

and:

p p1.zip(p2)
    .reject{ |_, v| v == nil }
    .map{ |k,v| k & v }
    .map(&:chr)
 
["r", "`", " ", "p", "h", "\"", "(", " ", ")", "Q", "`", "b"]

Suma:

p p1.zip(p2)
    .reject{ |_, v| v == nil }
    .map{ |k,v| k + v }
    .map(&:chr)
 
["\xEC", "\xD7", "\x83", "\xEA", "\xE2", "\xA0", "\x94", "\x93", "\xA4", "\xD0", "\xC6", "\xE0"]

Różnica:

p p1.zip(p2)
    .reject{ |_, v| v == nil }
    .map{ |k,v| k - v }
 
[8, -15, -63, -10, -18, 52, 60, 49, 50, 22, 2, -4]

Nie zamieniałem na znaki, bo wartości są od siebie zbyt różne. Nie są też po jednej stronie zera, aby można je potraktować jako indeksy w alfabecie albo coś takiego.

Iloczyn:

p p1.zip(p2)
    .reject{ |_, v| v == nil }
    .map{ |k,v| k * v }
 
[13908, 11500, 3298, 13664, 12688, 5724, 4576, 4802, 6099, 10695, 9800, 12540]

Jedyne w miarę sensowne wyniki daje and i suma, ale suma daje zawyżone. Może modulo 128?

p p1.zip(p2)
    .reject{ |_, v| v == nil }
    .map{ |k,v| (k + v) % 128 }
    .map(&:chr)
 
["l", "W", "\x03", "j", "b", " ", "\x14", "\x13", "$", "P", "F", "`"]

Niestety nie.

A może to ciąg 8-bitowych wartości zakodowanych w strumieniu 7-bitowych wartości zakodowanych w 8-bitowych bajtach?

Heh, całkiem długi ten opis Ale do rzeczy: można zauważyć, że w całym zaszyfrowanym tekście, górny bit jest wolny. Istnieje wobec tego podejrzenie, że jest to bit nieużywany.

Inaczej mówiąc:

p SECRET.map{|n| '%07d' % n.to_s(2).to_i }
 
[
    "1111010", "1100100", "0100010", "1110000",
    "1101000", "1101010", "1101000", "1100010",
    "1101011", "1110011", "1100100", "1101110",
    "1101011", "1100010", "0101110", "1000011",
    "1111111", "1110010", "1110011", "1100001",
    "1111010", "1111010", "0110110", "0101100",
    "0110001", "0111001", "1011101", "1100010",
    "1110010"
]

można zamienić na sekwencję 8-bitowych bajtów:

p SECRET
    .map{|n| '%07d' % n.to_s(2).to_i }
    .join
    .each_codepoint
    .each_slice(8)
    .map{ |a| a.map(&:chr).join }
 
[
    "11110101", "10010001", "00010111", "00001101",
    "00011010", "10110100", "01100010", "11010111",
    "11001111", "00100110", "11101101", "01111000",
    "10010111", "01000011", "11111111", "11001011",
    "10011110", "00011111", "01011110", "10011011",
    "00101100", "01100010", "11100110", "11101110",
    "00101110", "010"
]

Co po zamianie na tekst (UTF-8) daje:

p SECRET
    .map{|n| '%07d' % n.to_s(2).to_i }
    .join
    .each_codepoint
    .each_slice(8)
    .map{ |a| a.map(&:chr).join }
    .slice(0...-1)
    .map{ |n| n.to_i(2).chr }
    .join
 
"\xF5\x91\x17\r\x1A\xB4b\xD7\xCF&\xEDx\x97C\xFF\xCB\x9E\x1F^\x9B,b\xE6\xEE."

Czyli znów źle.

Przesunięcie wartości

Może wszystkie wartości należy zmienić o określoną wartość? Coś jak ROT-n, tylko dla wszystkich wartości bajtów.
Zarówno

(0..256).each do |n|
    p [n, SECRET.map{|c| ((256 + n + c) % 256).chr }.join]
end

jak i

(0..256).each do |n|
    p [n, SECRET.map{|c| ((256 + n i c) % 256).chr }.join]
end

nie przyniosły oczekiwanych rezultatów. Wyników nie umieszczę, bo jest ich zwyczajnie za dużo.

Szyfr Vigenère

ARR = (0...256).map{ |n|
    (0...256).to_a.rotate(n)
}.to_a
 
def vinegere(codepoints, first_letter)
    codepoints
        .each_with_object({ str: [], pass: first_letter }) do |val, obj|
            l = ARR[val][obj[:pass]]
            obj[:str].push l
            obj[:pass] = l
        end[:str]
        .map(&:chr)
        .join
end
 
(0..255).each do |c|
    p vinegere SECRET, c
end

Wyników znów jest za dużo aby je zamieszczać, ale to również nie to.

Mnożenie/dzielenie/potęgowanie treści jako liczby

Byłem przekonany, że nie dostanę tego samego co wysłałem. Jednak z braku innych pomysłów spróbowałem.

Funkcja zamieniająca liczbę na string:

def num_to_str(num)
    str = num.to_i.to_s(16)
    str = '0%s' % str unless str.size % 2 == 0
    str.each_codepoint.each_slice(2).map{ |n| n.map(&:chr).join.to_i(16).chr }.join
end
NUM = SECRET.map{ |n| n.to_s(16) }.join.to_i(16)
 
(1..20).each do |n|
    p [n, num_to_str(NUM ** n)]
end
 
(1..256).each do |n|
    p [n, num_to_str(NUM * n)]
end
 
(1..20).each do |n|
    p [n, num_to_str(NUM / n)]
end

Żadne nie przyniosło rezultatu. Mnożenie przez liczby większe niż 256 nie ma sensu, ponieważ mnożenie przez 256 dodaje po prostu pusty bajt na końcu.

Przyjrzyjmy się bliżej danym

Reprezentacja ASCII części tajemnej wygląda następująco:

zd"phjhbksdnkb.Crsazz6,19]br

Można zauważyć, że pierwszy znak, z, jest prawdopodobnie identyczny w wersji odkodowanej, ponieważ docelowy tekst to “Pozdrowienia z …” albo “Pozdrowienia ze …”. Następnym znakiem jest d – o jeden niżej od spodziewanego e. Kolnym jest , oddalony w ASCII jest o dwie pozycje od spacji. A jeszcze kolejnym jest p, oddalone w ASCII jest o trzy pozycje od s. Kolejne wartości, oraz w miarę sensowna treść (“ze słonecznego” lub “ze szwajcar…” jako potencjalne stringi)

x = [-0, 1, -2, 3]
 
p (0...(SECRET.size))
    .each_with_index
    .map{ |n, i| (SECRET[i].ord + x[i % x.size]).chr }
    .join
 
"ze shkfektbqkc,F\x7Fsqdz{4/1:[er"

Niestety czwórka już nie pasowała. Podjąłem też próbę z potęgami dwójki (1,2,4,8) w wariantach dodatnich i ujemnych, ale to też nie przyniosło rezultatu.

Rozwiązanie

Rozwiązanie okazało się bardzo zbliżone do ostatniego: zamiast dodawania, trzeba było xorować:

p (0...(SECRET.size))
    .each_with_index
    .map{ |n, i| (SECRET[i].ord ^ i).chr }
    .join
 
"ze slonecznego Locarno ;) Gyn"

Aż facepalm się ciśnie na czoło patrząc jak proste to powinno być. Ale ważne, że się udało!

Dzięki za pocztówkę, Gyn! (ale ubaw musiałeś mieć przedni, patrząc ile w logach mam ;​) i mhm)

Leave a Reply

Your email address will not be published.