Coverage for kryptic_cypher/cypher/bacons.py: 93%

69 statements  

« prev     ^ index     » next       coverage.py v7.14.2, created at 2026-06-20 18:52 +0000

1import string 

2 

3from kryptic_cypher.cypher.base import CypherResult, CypherWithKey, ValidationResult 

4 

5characters = { 

6 "A": 0, 

7 "B": 1, 

8 "C": 2, 

9 "D": 3, 

10 "E": 4, 

11 "F": 5, 

12 "G": 6, 

13 "H": 7, 

14 "I": 8, 

15 "J": 8, 

16 "K": 9, 

17 "L": 10, 

18 "M": 11, 

19 "N": 12, 

20 "O": 13, 

21 "P": 14, 

22 "Q": 15, 

23 "R": 16, 

24 "S": 17, 

25 "T": 18, 

26 "U": 19, 

27 "V": 19, 

28 "W": 20, 

29 "X": 21, 

30 "Y": 22, 

31 "Z": 23, 

32} 

33 

34 

35def sanitize_message(message: str) -> str: 

36 new_message = "" 

37 for letter in message.upper(): 

38 if string.ascii_letters.find(letter) != -1: 

39 new_message += letter 

40 return new_message 

41 

42 

43class BaconsCypher(CypherWithKey): 

44 """ 

45 A keyed cypher that encodes only text using a key to encode the binary value of a letter with a 5 bit value. 

46 The key's casing is changed to upper or lower depending on the binary value i.e. upper = 1, lower = 0. 

47 

48 The following table defines what each binary 5 bit value means for each letter: 

49 

50 | Letter | Code | Int Value | 

51 | ------ | ---- | --------- | 

52 | A | 00000 | 0 | 

53 | B | 00001 | 1 | 

54 | C | 00010 | 2 | 

55 | D | 00011 | 3 | 

56 | E | 00100 | 4 | 

57 | F | 00101 | 5 | 

58 | G | 00110 | 6 | 

59 | H | 00111 | 7 | 

60 | I,J | 01000 | 8 | 

61 | K | 01001 | 9 | 

62 | L | 01010 | 10 | 

63 | M | 01011 | 11 | 

64 | N | 01100 | 12 | 

65 | O | 01101 | 13 | 

66 | P | 01110 | 14 | 

67 | Q | 01111 | 15 | 

68 | R | 10000 | 16 | 

69 | S | 10001 | 17 | 

70 | T | 10010 | 18 | 

71 | U,V | 10011 | 19 | 

72 | W | 10100 | 20 | 

73 | X | 10101 | 21 | 

74 | Y | 10110 | 22 | 

75 | Z | 10111 | 23 | 

76 

77 **Examples** 

78 

79 'Hello World', 'Jared' -> '00111 00100 01010 01010 01101 10100 01101 10000 01010 00011' -> 'jaRED jaRed jArEd jArEd jAReD JaRed jAReD Jared jArEd jarED' 

80 'Hello World', 'sm' -> '00111 00100 01010 01010 01101 10100 01101 10000 01010 00011' -> 'sm SM Sm sM sm sM sM' 

81 'Hello World', 'supercoolkeythatwillbeawesome' -> '00111 00100 01010 01010 01101 10100 01101 10000 01010 00011' -> 'suPERcoOlkeYtHatWiLlbEAwESoMe suPErCOolkeyThAtwilLB' 

82 """ 

83 

84 @staticmethod 

85 def get_name() -> str: 

86 return __name__.split(".")[-1] 

87 

88 def validate_key(self, key: str) -> ValidationResult: 

89 if key is None or len(key) == 0: 

90 return ValidationResult.fail("Key cannot be empty.") 

91 

92 if len([c for c in key if c in string.ascii_letters]) < 2: 

93 return ValidationResult.fail( 

94 "Key must be at least 2 characters long with proper ascii letters." 

95 ) 

96 return ValidationResult.ok() 

97 

98 def encode(self, text: str, key: str) -> CypherResult: 

99 # Remove unsupported characters 

100 sanitized = sanitize_message(text) 

101 

102 # Is there anything to encode? 

103 if len(sanitized) == 0: 

104 return CypherResult.fail(sanitized, "Message cannot be empty.") 

105 

106 key_index = 0 

107 encoded = "" 

108 

109 # Encode each character 

110 for character in sanitized: 

111 value = characters[character] 

112 

113 # Turn the value into a 5 bit string 

114 for c in f"{value:05b}": 

115 

116 # Check to see if we've reached the end of the key 

117 if key_index >= len(key): 

118 key_index = 0 

119 encoded += " " 

120 

121 # Get the character from the key 

122 visual = key[key_index] 

123 while visual not in string.ascii_letters: 

124 if visual in string.whitespace: 

125 encoded += visual 

126 key_index += 1 

127 if key_index >= len(key): 

128 key_index = 0 

129 encoded += " " 

130 visual = key[key_index] 

131 key_index += 1 

132 

133 # Determine casing 

134 if c == "1": 

135 encoded += visual.upper() 

136 else: 

137 encoded += visual.lower() 

138 

139 return CypherResult.ok( 

140 sanitized, 

141 encoded, 

142 ) 

143 

144 def decode(self, text: str, key: str) -> CypherResult: 

145 pieces = text.split(" ") 

146 # Check to make sure the text is encoded with the given key 

147 if set(c.lower() for c in key if c in string.ascii_letters) != set( 

148 "".join(pieces).lower() 

149 ): 

150 return CypherResult.fail( 

151 text, 

152 "Text is not encoded with the given key, cannot trust the decoded text.", 

153 ) 

154 joined = "".join(pieces) 

155 

156 # Check that the text is a multiple of 5 since we encode 5 bits at a time 

157 if len(joined) % 5 != 0: 

158 return CypherResult.fail( 

159 text, 

160 "Text is not properly encoded into 5 bit values, cannot trust the decoded text.", 

161 ) 

162 

163 message = "" 

164 for i in range(0, len(joined), 5): 

165 section = joined[i : i + 5] 

166 # Turn section into binary 

167 binary = "" 

168 for character in section: 

169 if character.isupper(): 

170 binary += "1" 

171 else: 

172 binary += "0" 

173 true_value = int(binary, 2) 

174 

175 # Deduce letter from binary and make sure to account for collisions 

176 possible_keys = [] 

177 for key in characters.keys(): 

178 if characters[key] == true_value: 

179 possible_keys.append(key) 

180 

181 # Add the letter to the output message 

182 if len(possible_keys) == 1: 

183 message += possible_keys[0] 

184 else: 

185 message += f"({', '.join(possible_keys)})" 

186 

187 # Return the final assmbled message 

188 return CypherResult.ok(text, message)