1717# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
1818# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
1919
20+ import base64
21+ import binascii
22+ import hashlib
23+
24+ from . import crypto
25+
26+
27+ DIGEST = hashlib .md5
28+ DIGEST_LEN = 16
29+
2030
2131class ButtonsRow :
2232 """A row of an inline keyboard"""
2333
24- def __init__ (self ):
34+ def __init__ (self , bot ):
2535 self ._content = []
36+ self ._bot = bot
2637
2738 def url (self , label , url ):
2839 """Open an URL when the button is pressed"""
2940 self ._content .append ({"text" : label , "url" : url })
3041
3142 def callback (self , label , callback , data = None ):
3243 """Trigger a callback when the button is pressed"""
33- if data is not None :
34- msg = "%s\0 %s" % (callback , data )
35- else :
36- msg = callback
37-
38- self ._content .append ({"text" : label , "callback_data" : msg })
44+ self ._content .append ({
45+ "text" : label ,
46+ "callback_data" : get_callback_data (self ._bot , callback , data ),
47+ })
3948
4049 def switch_inline_query (self , label , query = "" , current_chat = False ):
4150 """Switch the user to this bot's inline query"""
@@ -54,12 +63,13 @@ def switch_inline_query(self, label, query="", current_chat=False):
5463class Buttons :
5564 """Factory for inline keyboards"""
5665
57- def __init__ (self ):
66+ def __init__ (self , bot ):
5867 self ._rows = {}
68+ self ._bot = bot
5969
6070 def __getitem__ (self , index ):
6171 if index not in self ._rows :
62- self ._rows [index ] = ButtonsRow ()
72+ self ._rows [index ] = ButtonsRow (self . _bot )
6373 return self ._rows [index ]
6474
6575 def _serialize_attachment (self ):
@@ -72,27 +82,71 @@ def _serialize_attachment(self):
7282 return {"inline_keyboard" : rows }
7383
7484
75- def buttons ( ):
76- """Create a new inline keyboard """
77- return Buttons ( )
85+ def parse_callback_data ( bot , raw ):
86+ """Parse the callback data generated by botogram and return it """
87+ raw = raw . encode ( "utf-8" )
7888
89+ if len (raw ) < 32 :
90+ raise crypto .TamperedMessageError
7991
80- def parse_callback_data (data ):
81- """Parse the callback data generated by botogram and return it"""
82- if "\0 " in data :
83- name , custom = data .split ("\0 " , 1 )
84- return name , custom
85- else :
86- return data , None
92+ try :
93+ prelude = base64 .b64decode (raw [:32 ])
94+ except binascii .Error :
95+ raise crypto .TamperedMessageError
96+
97+ signature = prelude [:16 ]
98+ name = prelude [16 :]
99+ data = raw [32 :]
100+
101+ if not crypto .compare (crypto .get_hmac (bot , name + data ), signature ):
102+ raise crypto .TamperedMessageError
103+
104+ return name , data .decode ("utf-8" )
105+
106+
107+ def get_callback_data (bot , name , data = None ):
108+ """Get the callback data for the provided name and data"""
109+ name = hashed_callback_name (name )
110+
111+ if data is None :
112+ data = ""
113+ data = data .encode ("utf-8" )
114+
115+ if len (data ) > 32 :
116+ raise ValueError (
117+ "The provided data is too big (%s bytes), try to reduce it to "
118+ "32 bytes" % len (data )
119+ )
120+
121+ # Get the signature of the hook name and data
122+ signature = crypto .get_hmac (bot , name + data )
123+
124+ # Base64 the signature and the hook name together to save space
125+ return (base64 .b64encode (signature + name ) + data ).decode ("utf-8" )
126+
127+
128+ def hashed_callback_name (name ):
129+ """Get the hashed name of a callback"""
130+ # Get only the first 8 bytes of the hash to fit it into the payload
131+ return DIGEST (name .encode ("utf-8" )).digest ()[:8 ]
87132
88133
89134def process (bot , chains , update ):
90135 """Process a callback sent to the bot"""
136+ try :
137+ name , data = parse_callback_data (bot , update .callback_query ._data )
138+ except crypto .TamperedMessageError :
139+ bot .logger .warn (
140+ "The user tampered with the #%s update's data. Skipped it."
141+ % update .update_id
142+ )
143+ return
144+
91145 for hook in chains ["callbacks" ]:
92146 bot .logger .debug ("Processing update #%s with the hook %s" %
93147 (update .update_id , hook .name ))
94148
95- result = hook .call (bot , update )
149+ result = hook .call (bot , update , name , data )
96150 if result is True :
97151 bot .logger .debug ("Update #%s was just processed by the %s hook" %
98152 (update .update_id , hook .name ))
0 commit comments