Hey everyone, winner here, in this post I’m going to share my method of how I’ve solved it. The ciphertexts seemed simpler than I initially thought, but with some additional rules that I didn’t expect at first. I’ve written Python scripts to work with the puzzle to get the solution, but they were done quickly and in a very terrifying manner, so let me warn you about the code spaghetti. Also
spoiler warning for those who want to solve on their own.
First of all, we need to define the character set in the black card, and a way to get which letter can correspond to them. We need to create a function that gets combinations of 12 silver card placements, while also defining silver card sides A and B, char at index 0 meaning first silver card char on top left most corner, index 1 to the right of the previous one etc. :
char_set = [r"0874923156Xgwk?",
r"Xm9+/sN^pdGb7ZS",
r"nu=iH!tGRehD/Bv",
r"y!624lCFe>8A@tM",
r"mzf}yjTZxc)]*-M",
r"V~LvET-5*Q&uPj#",
r"IrblN~U+CYWJP",
r"ciwfUKrnJqHBzh<",
r"%FLpSE r"a?RIW$1Y3g%x=kD",
r"a$q%?5wA^mV7n9>"]
silver_a = r"1234567890-+=~!@#$%^&*<>?/abcdefghijklmnopqrstuvwxyz"
silver_b = r"1234567890-+=qwertyuiop&^$asdfghjkl@#*%zxcvbnm<>?/~!"
combs = [[0, 2, 4, 6, 8], [2, 4, 6, 8, 10], [1, 3, 5, 7, 9]]
results = []
for comb in combs:
for n in range(0, 2):
comb_st = ""
for list_ind in comb:
if n == 0:
comb_st += (char_set[list_ind][:-1])
if n == 1:
comb_st += (char_set[list_ind][1:])
results.append(comb_st)
def get_comb(ind):
return results[ind]
print(get_comb(0))
# this returns ‘0874923156Xgwknu=iH!tGRehD/Bmzf}yjTZxc)]*-IrblN~U+CYWJ%FLpSE
def get_char_index(char, side):
if side == 0:
return silver_a.index(char)
else:
return silver_b.index(char)
print(get_char_index("q", 1)) # returns 13 for 1, and 42 for 0 in the second param
Now, we have a function to set combinations of strings for each placement, and we need to define another function to input a silver card character, and get back possible 4 connections on any given card placement combination:
def get_cons(ind, comb):
# Get connections - ind+1, ind+2, ind+14, ind+15
if ind > 12: # without this, the order of connections gets ruined
ind = ind + (ind // 13)
return [comb[ind], comb[ind+1], comb[ind+14], comb[ind+15]]
I was mostly manually trying around different combinations and placements using the functions above. There are some really interesting two blocks in C1:
JEQLJY
rEQL5U
We know that the first 4 letters are used from the hints, and if we need to use one card placement (out of 12) for all of the ciphertext, then these two blocks must have similar words. Namely:
J r i s k Y
b r i s 5 U
Now we have an idea of what kind of a structure we’re working with. 4 letters are used, and they shift left with every word by one character.
In my program, I wrote a simple for loop to loop over known seed phrase word letters, and I kind of got lucky looking at the connecting black card chars for them. In one of the last combinations, they map perfectly.
However, the order of choosing 4 possible characters in the silver card for a given black card character differs. How do you find that out? Well, I’m still not sure what the first 3 and the last 3 characters in C1 and C2 mean, but they might be about choosing the placement, and choosing the cycle in which 1 out of 4 letters to choose every character in the block, although these both variables are different for C1 and C2. Looking at possible mapping for that last one placement in C1, this pattern shows up:
234134
URU/Yw
Assigning one of the 4 directions (top left, top right, bottom left, bottom right), each character in the ciphers are assigned these, which determine the connecting silver card character to choose.
My next script originally iterated through all possible combinations, that's why it has 0,6 and 0,2. 0,6 for all the ways a silver card can be placed, and 0,2 for two sides of the silver card. Below I define a function to input a black card character “chrc”, and a number “num” for position encoding in the cipher block:
def get_slv(num, chrc):
rslt = ""
n = 0 # (5,6), (1, 2), (3, 4), (0, 1) C1 and C2 use these specific combinations for c and s
for c in range(5, 6):
for s in range(1, 2):
for letter in silver_a: # only need one side to get silver card chars
n += 1
comb0 = get_comb(c)
char1 = get_char_index(letter, side=s)
slv = get_cons(char1, comb0)
if slv[num-1] == chrc:
rslt += letter
return rslt
Now we can input any black character we want, and we get all the possible combinations if we set c to range from 0 to 6, and s to 0, 2. But to decipher C1, we need to take the specific combination 5,6 to 1,2, which corresponds to one of 12 card placement and side to choose. I believe I discovered the combination by luck, as it happened to be the last one being printed on my Python console.
But which block should we start from? We know that 4 letters are used for each seed phrase, and C1 and C2 both start with “URU” and end with “5ef”. So perhaps we can start with the second block, right?
Using the function above, and setting the block character values for each position “234134”, we get the first word as follows:
234134
LwE~T1
aseatb
Very interesting! Now let’s try to apply this algorithm to all of ciphertext C1, with the script below:
(sorry for horrible variable naming)
strn = old_cipher
pos_enc = "234134"
mapping = {0: int(pos_enc[0]),
1: int(pos_enc[1]),
2: int(pos_enc[2]),
3: int(pos_enc[3]),
4: int(pos_enc[4]),
5: int(pos_enc[5])}
count = 0
newst = ""
for l in strn:
pos = mapping[count]
result = get_slv(pos, l)
count += 1
if count > 5:
count = 0
#newst += "\n"
elif count == 1:
newst += "\n"
try:
newst += result
except TypeError:
newst += "_"
print(newst)
And when we input the C1 stored in old_cipher variable, we get this result:
cxd4ma
aseatb
propcs
emipca
t3iare
nelate
mriskn
brisud
esplca
ttotoo
ltrepe
v9xj8s
Incredible! We can see all the words! But hold on, there are no numbers or anything to mark the orders? Without the order, these words would be completely useless, as the possible combinations of these words are way too many to bruteforce.
Let’s try to write down the single characters between each word, shall we?
a-b-c-p-3-i-e-m-n-u-l-o-t
Now what does this mean? Well, we have the word “incomputable” on silver cards. This word determines the order of words.
i-n-c-o-m-p-u-t-a-b-l-e
1-2-3-4-5-6-7-8-9-10-11-12
When you use this knowledge to put the words in order, you get the words and the correct word order. Now, that we have an idea of how C1 is made, we can input the program the ciphertext C2, and let’s see the result:
cxd9um
85r
r567
8g
<75
87dtlk
kf
k%fl%
rv7d670
5%%
u8bj8s
Well… That didn’t work as expected. This must mean the block characters have a different mapping, and a different placement out of 12 is used. At this part of the puzzle, I was just manually printing different combinations and pos values. But the interesting thing is, you can sort of approach the correct plaintext slowly for C2 by modifying one thing at a time. It turns out, the mapping for C2 is “312134”, and combination (3,4) (0,1) is used, so c=3, side=0.
%8eyr
lwiset
pridac
lusuic
oniski
lnecon
pmargb
mirroe
nromsk
inedas
hcrive
91
Wow. It fits perfectly, and the letters of incomputable are all also there. So when you put them together in the correct order, we get the 12 seed phrase words in correct order for C2:
skill economy river enroll skin margin icon pride cluster mirror wise dash
However, there’s one more problem. The checksum isn’t correct! This seed phrase is invalid! But all of them are valid BIP39 words! Well, again perhaps to my luck, I was trying different word combinations (maybe pride is price? prize? Another layer to get the sats?)
I remembered that there might be 3-letter words, and I tried the easiest thing I can think of: ski instead of skin.
And that was it!
Thanks again OP for this challenge, and I’m looking forward to the next one. There’s still two questions left in my mind: How do we choose which card placement to choose? How are cipher blocks connection choices determined? I believe the answer might be in the first and last blocks of the ciphers. Perhaps I will try to look into it more.