1 """Enciphering and deciphering using the [Bifid cipher](https://en.wikipedia.org/wiki/Bifid_cipher).
2 Also attempts to break messages that use a Bifid cipher.
5 from szyfrow
.support
.utilities
import *
6 from szyfrow
.support
.language_models
import *
7 from szyfrow
.keyword_cipher
import KeywordWrapAlphabet
, keyword_cipher_alphabet_of
9 def bifid_grid(keyword
, wrap_alphabet
, letter_mapping
):
10 """Create the grids for a Bifid cipher
12 cipher_alphabet
= keyword_cipher_alphabet_of(keyword
, wrap_alphabet
)
13 if letter_mapping
is None:
14 letter_mapping
= {'j': 'i'}
15 translation
= ''.maketrans(letter_mapping
)
16 cipher_alphabet
= cat(collections
.OrderedDict
.fromkeys(cipher_alphabet
.translate(translation
)))
17 f_grid
= {k
: ((i
// 5) + 1, (i
% 5) + 1)
18 for i
, k
in enumerate(cipher_alphabet
)}
19 r_grid
= {((i
// 5) + 1, (i
% 5) + 1): k
20 for i
, k
in enumerate(cipher_alphabet
)}
21 return translation
, f_grid
, r_grid
23 def bifid_encipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
,
24 letter_mapping
=None, period
=None, fillvalue
=None):
27 >>> bifid_encipher("indiajelly", 'iguana')
29 >>> bifid_encipher("indiacurry", 'iguana', period=4)
31 >>> bifid_encipher("indiacurry", 'iguana', period=4, fillvalue='x')
37 raise ValueError("fillvalue must be given if period is given")
39 p_message
= message
+ pad(len(message
), period
, fillvalue
)
43 translation
, f_grid
, r_grid
= bifid_grid(keyword
, wrap_alphabet
, letter_mapping
)
45 t_message
= p_message
.translate(translation
)
46 pairs0
= [f_grid
[l
] for l
in sanitise(t_message
)]
48 # chunked_pairs = [pairs0[i:i+period] for i in range(0, len(pairs0), period)]
49 # if len(chunked_pairs[-1]) < period and fillvalue:
50 # chunked_pairs[-1] += [f_grid[fillvalue]] * (period - len(chunked_pairs[-1]))
51 chunked_pairs
= chunks(pairs0
, period
, fillvalue
=None)
53 chunked_pairs
= [pairs0
]
56 for c
in chunked_pairs
:
57 items
= sum(list(list(i
) for i
in zip(*c
)), [])
58 p
= [(items
[i
], items
[i
+1]) for i
in range(0, len(items
), 2)]
61 return cat(r_grid
[p
] for p
in pairs1
)
64 def bifid_decipher(message
, keyword
, wrap_alphabet
=KeywordWrapAlphabet
.from_a
,
65 letter_mapping
=None, period
=None, fillvalue
=None):
66 """Decipher with bifid cipher
68 >>> bifid_decipher('ibidonhprm', 'iguana')
70 >>> bifid_decipher("ibnhgaqltm", 'iguana', period=4)
72 >>> bifid_decipher("ibnhgaqltzml", 'iguana', period=4)
77 raise ValueError("fillvalue must be given if period is given")
79 p_message
= message
+ pad(len(message
), period
, fillvalue
)
83 translation
, f_grid
, r_grid
= bifid_grid(keyword
, wrap_alphabet
, letter_mapping
)
85 t_message
= message
.translate(translation
)
86 pairs0
= [f_grid
[l
] for l
in sanitise(t_message
)]
88 # chunked_pairs = [pairs0[i:i+period] for i in range(0, len(pairs0), period)]
89 # if len(chunked_pairs[-1]) < period and fillvalue:
90 # chunked_pairs[-1] += [f_grid[fillvalue]] * (period - len(chunked_pairs[-1]))
91 chunked_pairs
= chunks(pairs0
, period
, fillvalue
=None)
93 chunked_pairs
= [pairs0
]
96 for c
in chunked_pairs
:
97 items
= [j
for i
in c
for j
in i
]
99 p
= [(items
[i
], items
[i
+gap
]) for i
in range(gap
)]
102 return cat(r_grid
[p
] for p
in pairs1
)
105 def bifid_break(message
, wordlist
=None, fitness
=Pletters
, max_period
=10,
106 number_of_solutions
=1, chunksize
=500):
107 """Breaks a keyword substitution cipher using a dictionary and
110 If `wordlist` is not specified, use
111 [`szyfrow.support.langauge_models.keywords`](support/language_models.html#szyfrow.support.language_models.keywords).
113 >>> bifid_break(bifid_encipher('this is a test message for the ' \
114 'keyword decipherment', 'elephant', wrap_alphabet=KeywordWrapAlphabet.from_last), \
115 wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
116 (('elephant', <KeywordWrapAlphabet.from_last: 2>, 0), -52.834575011...)
117 >>> bifid_break(bifid_encipher('this is a test message for the ' \
118 'keyword decipherment', 'elephant', wrap_alphabet=KeywordWrapAlphabet.from_last), \
119 wordlist=['cat', 'elephant', 'kangaroo'], \
120 number_of_solutions=2) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
121 [(('elephant', <KeywordWrapAlphabet.from_last: 2>, 0), -52.834575011...),
122 (('elephant', <KeywordWrapAlphabet.from_largest: 3>, 0), -52.834575011...)]
127 with multiprocessing
.Pool() as pool
:
128 helper_args
= [(message
, word
, wrap
, period
, fitness
)
130 for wrap
in KeywordWrapAlphabet
131 for period
in range(max_period
+1)]
132 # Gotcha: the helper function here needs to be defined at the top level
133 # (limitation of Pool.starmap)
134 breaks
= pool
.starmap(bifid_break_worker
, helper_args
, chunksize
)
135 if number_of_solutions
== 1:
136 return max(breaks
, key
=lambda k
: k
[1])
138 return sorted(breaks
, key
=lambda k
: k
[1], reverse
=True)[:number_of_solutions
]
140 def bifid_break_worker(message
, keyword
, wrap_alphabet
, period
, fitness
):
141 plaintext
= bifid_decipher(message
, keyword
, wrap_alphabet
,
142 period
=period
, fillvalue
='e')
143 fit
= fitness(plaintext
)
144 return (keyword
, wrap_alphabet
, period
), fit
146 if __name__
== "__main__":