diff --git a/pycrate_mobile/TS23038.py b/pycrate_mobile/TS23038.py index cc5e29e..d99588e 100644 --- a/pycrate_mobile/TS23038.py +++ b/pycrate_mobile/TS23038.py @@ -117,6 +117,7 @@ class SMS_DCS(Envelope): Uint('Charset', bl=2), Uint('Class', bl=2) ) + def __init__(self, *args, **kwargs): Envelope.__init__(self, *args, **kwargs) self[1].set_dicauto(self._set_cs_dic) @@ -256,301 +257,305 @@ def _set_cla_dic(self): #------------------------------------------------------------------------------# _GSM7bLUT = { - 0 : u'@', - 1 : u'£', - 2 : u'$', - 3 : u'¥', - 4 : u'è', - 5 : u'é', - 6 : u'ù', - 7 : u'ì', - 8 : u'ò', - 9 : u'Ç', - 10 : u'\n', - 11 : u'Ø', - 12 : u'ø', - 13 : u'\r', - 14 : u'Å', - 15 : u'å', - 16 : u'Δ', - 17 : u'_', - 18 : u'Φ', - 19 : u'Γ', - 20 : u'Λ', - 21 : u'Ω', - 22 : u'Π', - 23 : u'Ψ', - 24 : u'Σ', - 25 : u'Θ', - 26 : u'Ξ', - 27 : u'\x1b', - 28 : u'Æ', - 29 : u'æ', - 30 : u'ß', - 31 : u'É', - 32 : u' ', - 33 : u'!', - 34 : u'"', - 35 : u'#', - 36 : u'¤', - 37 : u'%', - 38 : u'&', + 0 : '@', + 1 : '£', + 2 : '$', + 3 : '¥', + 4 : 'è', + 5 : 'é', + 6 : 'ù', + 7 : 'ì', + 8 : 'ò', + 9 : 'Ç', + 10 : '\n', + 11 : 'Ø', + 12 : 'ø', + 13 : '\r', + 14 : 'Å', + 15 : 'å', + 16 : 'Δ', + 17 : '_', + 18 : 'Φ', + 19 : 'Γ', + 20 : 'Λ', + 21 : 'Ω', + 22 : 'Π', + 23 : 'Ψ', + 24 : 'Σ', + 25 : 'Θ', + 26 : 'Ξ', + 27 : '\x1b', + 28 : 'Æ', + 29 : 'æ', + 30 : 'ß', + 31 : 'É', + 32 : ' ', + 33 : '!', + 34 : '"', + 35 : '#', + 36 : '¤', + 37 : '%', + 38 : '&', 39 : u"'", - 40 : u'(', - 41 : u')', - 42 : u'*', - 43 : u'+', - 44 : u',', - 45 : u'-', - 46 : u'.', - 47 : u'/', - 48 : u'0', - 49 : u'1', - 50 : u'2', - 51 : u'3', - 52 : u'4', - 53 : u'5', - 54 : u'6', - 55 : u'7', - 56 : u'8', - 57 : u'9', - 58 : u':', - 59 : u';', - 60 : u'<', - 61 : u'=', - 62 : u'>', - 63 : u'?', - 64 : u'¡', - 65 : u'A', - 66 : u'B', - 67 : u'C', - 68 : u'D', - 69 : u'E', - 70 : u'F', - 71 : u'G', - 72 : u'H', - 73 : u'I', - 74 : u'J', - 75 : u'K', - 76 : u'L', - 77 : u'M', - 78 : u'N', - 79 : u'O', - 80 : u'P', - 81 : u'Q', - 82 : u'R', - 83 : u'S', - 84 : u'T', - 85 : u'U', - 86 : u'V', - 87 : u'W', - 88 : u'X', - 89 : u'Y', - 90 : u'Z', - 91 : u'Ä', - 92 : u'Ö', - 93 : u'Ñ', - 94 : u'Ü', - 95 : u'§', - 96 : u'¿', - 97 : u'a', - 98 : u'b', - 99 : u'c', - 100 : u'd', - 101 : u'e', - 102 : u'f', - 103 : u'g', - 104 : u'h', - 105 : u'i', - 106 : u'j', - 107 : u'k', - 108 : u'l', - 109 : u'm', - 110 : u'n', - 111 : u'o', - 112 : u'p', - 113 : u'q', - 114 : u'r', - 115 : u's', - 116 : u't', - 117 : u'u', - 118 : u'v', - 119 : u'w', - 120 : u'x', - 121 : u'y', - 122 : u'z', - 123 : u'ä', - 124 : u'ö', - 125 : u'ñ', - 126 : u'ü', - 127 : u'à' + 40 : '(', + 41 : ')', + 42 : '*', + 43 : '+', + 44 : ',', + 45 : '-', + 46 : '.', + 47 : '/', + 48 : '0', + 49 : '1', + 50 : '2', + 51 : '3', + 52 : '4', + 53 : '5', + 54 : '6', + 55 : '7', + 56 : '8', + 57 : '9', + 58 : ':', + 59 : ';', + 60 : '<', + 61 : '=', + 62 : '>', + 63 : '?', + 64 : '¡', + 65 : 'A', + 66 : 'B', + 67 : 'C', + 68 : 'D', + 69 : 'E', + 70 : 'F', + 71 : 'G', + 72 : 'H', + 73 : 'I', + 74 : 'J', + 75 : 'K', + 76 : 'L', + 77 : 'M', + 78 : 'N', + 79 : 'O', + 80 : 'P', + 81 : 'Q', + 82 : 'R', + 83 : 'S', + 84 : 'T', + 85 : 'U', + 86 : 'V', + 87 : 'W', + 88 : 'X', + 89 : 'Y', + 90 : 'Z', + 91 : 'Ä', + 92 : 'Ö', + 93 : 'Ñ', + 94 : 'Ü', + 95 : '§', + 96 : '¿', + 97 : 'a', + 98 : 'b', + 99 : 'c', + 100 : 'd', + 101 : 'e', + 102 : 'f', + 103 : 'g', + 104 : 'h', + 105 : 'i', + 106 : 'j', + 107 : 'k', + 108 : 'l', + 109 : 'm', + 110 : 'n', + 111 : 'o', + 112 : 'p', + 113 : 'q', + 114 : 'r', + 115 : 's', + 116 : 't', + 117 : 'u', + 118 : 'v', + 119 : 'w', + 120 : 'x', + 121 : 'y', + 122 : 'z', + 123 : 'ä', + 124 : 'ö', + 125 : 'ñ', + 126 : 'ü', + 127 : 'à' } _GSM7bLUTInv = { - u'@' : 0, - u'£' : 1, - u'$' : 2, - u'¥' : 3, - u'è' : 4, - u'é' : 5, - u'ù' : 6, - u'ì' : 7, - u'ò' : 8, - u'Ç' : 9, - u'\n' : 10, - u'Ø' : 11, - u'ø' : 12, - u'\r' : 13, - u'Å' : 14, - u'å' : 15, - u'Δ' : 16, - u'_' : 17, - u'Φ' : 18, - u'Γ' : 19, - u'Λ' : 20, - u'Ω' : 21, - u'Π' : 22, - u'Ψ' : 23, - u'Σ' : 24, - u'Θ' : 25, - u'Ξ' : 26, - u'\x1b' : 27, - u'Æ' : 28, - u'æ' : 29, - u'ß' : 30, - u'É' : 31, - u' ' : 32, - u'!' : 33, - u'"' : 34, - u'#' : 35, - u'¤' : 36, - u'%' : 37, - u'&' : 38, + '@' : 0, + '£' : 1, + '$' : 2, + '¥' : 3, + 'è' : 4, + 'é' : 5, + 'ù' : 6, + 'ì' : 7, + 'ò' : 8, + 'Ç' : 9, + '\n' : 10, + 'Ø' : 11, + 'ø' : 12, + '\r' : 13, + 'Å' : 14, + 'å' : 15, + 'Δ' : 16, + '_' : 17, + 'Φ' : 18, + 'Γ' : 19, + 'Λ' : 20, + 'Ω' : 21, + 'Π' : 22, + 'Ψ' : 23, + 'Σ' : 24, + 'Θ' : 25, + 'Ξ' : 26, + '\x1b' : 27, + 'Æ' : 28, + 'æ' : 29, + 'ß' : 30, + 'É' : 31, + ' ' : 32, + '!' : 33, + '"' : 34, + '#' : 35, + '¤' : 36, + '%' : 37, + '&' : 38, u"'" : 39, - u'(' : 40, - u')' : 41, - u'*' : 42, - u'+' : 43, - u',' : 44, - u'-' : 45, - u'.' : 46, - u'/' : 47, - u'0' : 48, - u'1' : 49, - u'2' : 50, - u'3' : 51, - u'4' : 52, - u'5' : 53, - u'6' : 54, - u'7' : 55, - u'8' : 56, - u'9' : 57, - u':' : 58, - u';' : 59, - u'<' : 60, - u'=' : 61, - u'>' : 62, - u'?' : 63, - u'¡' : 64, - u'A' : 65, - u'B' : 66, - u'C' : 67, - u'D' : 68, - u'E' : 69, - u'F' : 70, - u'G' : 71, - u'H' : 72, - u'I' : 73, - u'J' : 74, - u'K' : 75, - u'L' : 76, - u'M' : 77, - u'N' : 78, - u'O' : 79, - u'P' : 80, - u'Q' : 81, - u'R' : 82, - u'S' : 83, - u'T' : 84, - u'U' : 85, - u'V' : 86, - u'W' : 87, - u'X' : 88, - u'Y' : 89, - u'Z' : 90, - u'Ä' : 91, - u'Ö' : 92, - u'Ñ' : 93, - u'Ü' : 94, - u'§' : 95, - u'¿' : 96, - u'a' : 97, - u'b' : 98, - u'c' : 99, - u'd' : 100, - u'e' : 101, - u'f' : 102, - u'g' : 103, - u'h' : 104, - u'i' : 105, - u'j' : 106, - u'k' : 107, - u'l' : 108, - u'm' : 109, - u'n' : 110, - u'o' : 111, - u'p' : 112, - u'q' : 113, - u'r' : 114, - u's' : 115, - u't' : 116, - u'u' : 117, - u'v' : 118, - u'w' : 119, - u'x' : 120, - u'y' : 121, - u'z' : 122, - u'ä' : 123, - u'ö' : 124, - u'ñ' : 125, - u'ü' : 126, - u'à' : 127 + '(' : 40, + ')' : 41, + '*' : 42, + '+' : 43, + ',' : 44, + '-' : 45, + '.' : 46, + '/' : 47, + '0' : 48, + '1' : 49, + '2' : 50, + '3' : 51, + '4' : 52, + '5' : 53, + '6' : 54, + '7' : 55, + '8' : 56, + '9' : 57, + ':' : 58, + ';' : 59, + '<' : 60, + '=' : 61, + '>' : 62, + '?' : 63, + '¡' : 64, + 'A' : 65, + 'B' : 66, + 'C' : 67, + 'D' : 68, + 'E' : 69, + 'F' : 70, + 'G' : 71, + 'H' : 72, + 'I' : 73, + 'J' : 74, + 'K' : 75, + 'L' : 76, + 'M' : 77, + 'N' : 78, + 'O' : 79, + 'P' : 80, + 'Q' : 81, + 'R' : 82, + 'S' : 83, + 'T' : 84, + 'U' : 85, + 'V' : 86, + 'W' : 87, + 'X' : 88, + 'Y' : 89, + 'Z' : 90, + 'Ä' : 91, + 'Ö' : 92, + 'Ñ' : 93, + 'Ü' : 94, + '§' : 95, + '¿' : 96, + 'a' : 97, + 'b' : 98, + 'c' : 99, + 'd' : 100, + 'e' : 101, + 'f' : 102, + 'g' : 103, + 'h' : 104, + 'i' : 105, + 'j' : 106, + 'k' : 107, + 'l' : 108, + 'm' : 109, + 'n' : 110, + 'o' : 111, + 'p' : 112, + 'q' : 113, + 'r' : 114, + 's' : 115, + 't' : 116, + 'u' : 117, + 'v' : 118, + 'w' : 119, + 'x' : 120, + 'y' : 121, + 'z' : 122, + 'ä' : 123, + 'ö' : 124, + 'ñ' : 125, + 'ü' : 126, + 'à' : 127 } +_GSM7bTab = '@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1bÆæßÉ !"#¤%&\'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà' + _GSM7bExtLUT = { - 10 : u'\x0c', - 13 : u'\x11', # no real equivalent to CR2 in the ascii table - 20 : u'^', - 27 : u'\x0e', # no real equivalent to SS2 in the ascii table - 40 : u'{', - 41 : u'}', - 47 : u'\\', - 60 : u'[', - 61 : u'~', - 62 : u']', - 64 : u'|', - 101 : u'€' + 10 : '\x0c', + 13 : '\x11', # no real equivalent to CR2 in the ascii table + 20 : '^', + 27 : '\x0e', # no real equivalent to SS2 in the ascii table + 40 : '{', + 41 : '}', + 47 : '\\', + 60 : '[', + 61 : '~', + 62 : ']', + 64 : '|', + 101 : '€' } _GSM7bExtLUTInv = { - u'\x0c' : 10, - u'\x11' : 13, - u'^' : 20, - u'\x0e' : 27, - u'{' : 40, - u'}' : 41, - u'\\' : 47, - u'[' : 60, - u'~' : 61, - u']' : 62, - u'|' : 64, - u'€' : 101 + '\x0c' : 10, + '\x11' : 13, + '^' : 20, + '\x0e' : 27, + '{' : 40, + '}' : 41, + '\\' : 47, + '[' : 60, + '~' : 61, + ']' : 62, + '|' : 64, + '€' : 101 } +_GSM7bExtTab = '\x0c\x11^\x0e{}\\[~]|€' + def encode_7b(txt, off=0): """translates the unicode string `txt' to a GSM 7 bit characters buffer - Enables the `txt' string to start at a non-null offset `off' as it is the case + Enables the encoded buffer to start at a non-null bit offset `off' as it is the case with fill bits after certain SMS User-Data-Headers Args: @@ -575,8 +580,15 @@ def encode_7b(txt, off=0): cnt += 2 else: cnt += 1 - # check the length in bits and add fill bits (for the UDH) - arr.insert(0, (TYPE_UINT, 0, off)) + # add fill bits at the front (for the UDH to align on septets) + arr.append((TYPE_UINT, 0, off)) + # add fill bits at the end (to align on a byte boundary) + padbl = (-off-cnt*7) % 8 + if padbl == 7: + # pad with a \r, to avoid including a padding septet that would decode as @ + arr.insert(0, (TYPE_UINT, 13, 7)) + else: + arr.insert(0, (TYPE_UINT, 0, padbl)) return bytes(reversed(pack_val(*arr)[0])), cnt @@ -592,11 +604,7 @@ def decode_7b(buf, off=0): Returns: decoded text string (utf8 str) """ - # WNG: in case of 7 remaining bits at the end of the buffer, we will have an @ - # at the end of the returned string - # we would need the chars_num parameter passed as function arg to fix this - # - char = Charpy(bytes(reversed(buf))) + char = Charpy(bytes(reversed(buf))) # # get the size and number of characters buf_bl = len(buf) << 3 @@ -614,15 +622,19 @@ def decode_7b(buf, off=0): try: chars[-1] = _GSM7bExtLUT[arr[i-1]] except KeyError: - chars.append(u' ') + chars.append(' ') else: chars.append(_GSM7bLUT[v]) - return u''.join(reversed(chars)) + # + if chars and chars[0] in ('@', '\r'): + # strip the last character corresponding to the last 7-bit padding (being 0x00 or 0x13) + return ''.join(reversed(chars[1:])) + else: + return ''.join(reversed(chars)) + -# TODO: we should test more thoroughly the 7b codec when a UDH is present and creates a misalignment -# Here are some wireshark filters to collect such kind of SMS -# gsm_sms.tp-dcs == 0 and gsm_sms.dis_field_udh.user_data_header_length != 6 and gsm_sms.ie_identifier != 0x00 -# gsm_sms.dis_field_udh.gsm.fill_bits == 0x0 and gsm_sms.ie_identifier != 0x00 +def decode_7b_gmr(buf): + pass def encode_7b_cbs(txt): @@ -672,5 +684,5 @@ def decode_7b_cbs(pages): txt = [] for page, page_len in pages: txt.append( decode_7b(page[:page_len]) ) - return u''.join(txt) + return ''.join(txt) diff --git a/pycrate_mobile/TS23040_SMS.py b/pycrate_mobile/TS23040_SMS.py index cf6efe1..60c3cd0 100644 --- a/pycrate_mobile/TS23040_SMS.py +++ b/pycrate_mobile/TS23040_SMS.py @@ -899,19 +899,13 @@ def __init__(self, *args, **kwargs): class BufUD(Buf): # default encoding, required in SMS REPORT without TP-DCS - DEFAULT_DCS = DCS_7B - # indicator for GSM 7b encoding length - _ENC_BL = 0 + DEFAULT_DCS = DCS_7B def set_val(self, val): if val is None: self._val = None elif isinstance(val, bytes_types): Buf.set_val(self, val) - if self.get_dcs() == DCS_7B: - self._ENC_BL = 8*len(val) - else: - self._ENC_BL = 0 else: self.encode(val) @@ -933,7 +927,7 @@ def get_dcs(self): return DCS_UCS return DCS_8B - def get_dcs7b_off(self): + def _get_dcs7b_off(self): try: udh = self.get_env()['UDH'] except Exception: @@ -949,27 +943,17 @@ def get_dcs7b_off(self): def encode(self, val): dcs = self.get_dcs() if dcs == DCS_7B: - enc, cnt = encode_7b(val, self.get_dcs7b_off()) + enc, cnt = encode_7b(val, self._get_dcs7b_off()) self.set_val(enc) - self._ENC_BL = 7*cnt elif dcs == DCS_UCS: self.set_val(val.encode('utf-16-be')) - self._ENC_BL = 0 else: self.set_val(val) - self._ENC_BL = 0 def decode(self): dcs = self.get_dcs() if dcs == DCS_7B: - try: - udhbl = self.get_env()['UDH'].get_bl() - except Exception: - udhbl = 0 - if (udhbl + self._ENC_BL) % 8 == 1: - return decode_7b(self.get_val(), self.get_dcs7b_off())[:-1] - else: - return decode_7b(self.get_val(), self.get_dcs7b_off()) + return decode_7b(self.get_val(), self._get_dcs7b_off()) elif dcs == DCS_UCS: return str(self.get_val(), 'utf-16-be') else: @@ -1002,10 +986,8 @@ def __init__(self, *args, **kwargs): def _set_udl(self): if self.get_dcs() == DCS_7B: - # count number of septets: - # UDH should be a round number of septet (thanks to the fill bits) - # UD should have a non-null ENC_BL after encoding the text - return (self[1].get_bl() + self[2]._ENC_BL) // 7 + # count number of septets (UDH + UD): + return (self[1].get_bl() + self[2].get_bl()) // 7 else: return self[1].get_len() + self[2].get_len() diff --git a/test/test_pycrate.py b/test/test_pycrate.py index d0c18aa..deb4a02 100644 --- a/test/test_pycrate.py +++ b/test/test_pycrate.py @@ -41,11 +41,18 @@ from test.test_asn1rt import * from test.test_mobile import * from test.test_gsmrr import * +from test.test_sms import * from test.test_crypto import * -from test.test_sedebugmux import * +from test.test_sedebugmux import * from pycrate_asn1c.specdir import ASN_SPECS -from pycrate_asn1c.asnproc import compile_text, compile_spec, compile_all, \ - generate_modules, PycrateGenerator, GLOBAL +from pycrate_asn1c.asnproc import ( + compile_text, + compile_spec, + compile_all, + generate_modules, + PycrateGenerator, + GLOBAL + ) from pycrate_asn1rt.asnobj import ASN1Obj Element._SAFE_STAT = True @@ -187,6 +194,11 @@ def test_gsmrr(self): test_gsmrr_l2_mt() test_gsmrr_mt() + # mobile / TS 23040 SMS and TS 23038 DU encoding + def test_sms(self): + print('[<>] testing SMS TP-UDH in pycrate_mobile') + test_tpudh() + # osmo related protocols def test_osmo(self): print('[<>] testing pycrate_osmo') @@ -214,6 +226,7 @@ def test_perf_all(): test_perf_csn1() test_perf_mobile() test_perf_gsmrr() + test_perf_sms() test_perf_sedebugmux() test_perf_crypto() print('[<<<>>>] test_perf_all total time: %.4f' % (time.time() - T0)) diff --git a/test/test_sms.py b/test/test_sms.py new file mode 100644 index 0000000..6c02303 --- /dev/null +++ b/test/test_sms.py @@ -0,0 +1,70 @@ +# -*- coding: UTF-8 -*- +#/** +# * Software Name : pycrate +# * Version : 0.6 +# * +# * Copyright 2023. Benoit Michau. P1Sec. +# * +# * This library is free software; you can redistribute it and/or +# * modify it under the terms of the GNU Lesser General Public +# * License as published by the Free Software Foundation; either +# * version 2.1 of the License, or (at your option) any later version. +# * +# * This library is distributed in the hope that it will be useful, +# * but WITHOUT ANY WARRANTY; without even the implied warranty of +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# * Lesser General Public License for more details. +# * +# * You should have received a copy of the GNU Lesser General Public +# * License along with this library; if not, write to the Free Software +# * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# * MA 02110-1301 USA +# * +# *-------------------------------------------------------- +# * File Name : test/test_sms.py +# * Created : 2023-11-20 +# * Authors : Benoit Michau +# *-------------------------------------------------------- +#*/ + +from binascii import unhexlify +from timeit import timeit + +from pycrate_mobile.TS23040_SMS import * + +# SMS 7 bit character together with UDH requires complex alignment and padding +# Here is a harness test to ensure everything gets encoded / decoded properly + +def test_tpudh(I=28, J=8): + for i in range(0, I): + for j in range(0, J): + txt = i*'A' + enc = SMS_SUBMIT(val={ + 'TP_UDHI': 1, + 'TP_DA' : {'Num': '33123456'}, + 'TP_DCS' : {'Group': 0, 'Charset': 0, 'Class': 0}, + 'TP_UD' : { + 'UDH' : {'UDH': [{'T': 2, 'V': j*b'\0'}]}, + 'UD' : txt + }}) + buf = enc.to_bytes() + dec = SMS_SUBMIT() + dec.from_bytes(buf) + assert( dec['TP_UD']['UD'].decode() in (txt, txt+'\r') ) + + +# For more real-cases testing, here are some wireshark filters to collect such kind of SMS +# gsm_sms.tp-dcs == 0 and gsm_sms.dis_field_udh.user_data_header_length != 6 and gsm_sms.ie_identifier != 0x00 +# gsm_sms.dis_field_udh.gsm.fill_bits == 0x0 and gsm_sms.ie_identifier != 0x00 + + +def test_perf_sms(): + + print('[+] SMS_SUBMIT encoding and decoding with GSM 7-bit and TP-UDH') + Ta = timeit(test_tpudh, number=5) + print('test_tpudh: {0:.4f}'.format(Ta)) + + +if __name__ == '__main__': + test_perf_sms() +