From 1d33f182f0676af2eaf43366cde45013c2398c75 Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Wed, 25 Sep 2024 09:14:47 -0500 Subject: [PATCH 1/2] Deprecate several utility functions In addition, * Add type annotations to utility functions * Update utility function doc strings * Use newer Python constructs to simplify remaining code --- ChangeLog | 2 + .../Examples/scard-api/sample_pinpad.py | 1 - src/smartcard/util/__init__.py | 140 ++++++++---------- test/test_util.py | 12 +- 4 files changed, 73 insertions(+), 82 deletions(-) diff --git a/ChangeLog b/ChangeLog index 8dc100da..db079b2d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,6 +8,8 @@ Unreleased changes * Standardize local and CI testing to use tox * Build wheels in CI for all supported Python versions * Build the docs as a part of the test suite + * Begin to add type annotations to the package + * Deprecate the `HexListToBinString`, `BinStringToHexList`, `hl2bs`, and `bs2hl` utility functions 2.1.1 (September 2024) ====================== diff --git a/src/smartcard/Examples/scard-api/sample_pinpad.py b/src/smartcard/Examples/scard-api/sample_pinpad.py index 276e13dd..5a629cee 100755 --- a/src/smartcard/Examples/scard-api/sample_pinpad.py +++ b/src/smartcard/Examples/scard-api/sample_pinpad.py @@ -26,7 +26,6 @@ """ from smartcard.scard import * -from smartcard.util import toASCIIBytes from smartcard.pcsc.PCSCExceptions import * import sys diff --git a/src/smartcard/util/__init__.py b/src/smartcard/util/__init__.py index 3777e53a..04eda5b4 100644 --- a/src/smartcard/util/__init__.py +++ b/src/smartcard/util/__init__.py @@ -22,13 +22,16 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """ +from __future__ import annotations +import warnings + PACK = 1 HEX = 2 UPPERCASE = 4 COMMA = 8 -def padd(bytelist, length, padding='FF'): +def padd(bytelist: list[int], length: int, padding: str = 'FF'): """ Padds a byte list with a constant byte value (default is x0FF) @param bytelist: the byte list to padd @param length: the total length of the resulting byte list; @@ -45,16 +48,11 @@ def padd(bytelist, length, padding='FF'): [59, 101, 0, 0, 156, 17, 1, 1, 3] """ - newlist = list(bytelist) - if len(newlist) < length: - for index in range(length - len(newlist)): - newlist.append(eval('0x' + padding)) - - return newlist + return bytelist + [int(padding, 16)] * (length - len(bytelist)) -def toASCIIBytes(stringtoconvert): - """Returns a list of ASCII bytes from a string. +def toASCIIBytes(stringtoconvert: str) -> list[int]: + """Convert a string to a list of UTF-8 encoded bytes. @param stringtoconvert: the string to convert into a byte list @@ -66,11 +64,13 @@ def toASCIIBytes(stringtoconvert): [78, 117, 109, 98, 101, 114, 32, 49, 48, 49] """ - return list(map(ord, list(stringtoconvert))) + return list(stringtoconvert.encode("utf-8")) + +def toASCIIString(bytelist: list[int]) -> str: + """Convert a list of integers in the range ``[32, 127]`` to a string. -def toASCIIString(bytelist): - """Returns a string representing a list of ASCII bytes. + Integer values outside the range ``[32, 127]`` are replaced with a period. @param bytelist: list of ASCII bytes to convert into a string @@ -84,20 +84,16 @@ def toASCIIString(bytelist): ". .~." """ - res = [] - for b in bytelist: - if b < 32 or b > 127: - c = '.' - else: - c = chr(b) - res.append(c) - return ''.join(res) + return ''.join( + chr(c) if 32 <= c <= 127 else '.' + for c in bytelist + ) -def toBytes(bytestring): - """Returns a list of bytes from a byte string +def toBytes(bytestring: str) -> list[int]: + """Convert a string of hexadecimal characters to a list of integers. - bytestring: a byte string + @param bytestring: a byte string >>> toBytes("3B 65 00 00 9C 11 01 01 03") [59, 101, 0, 0, 156, 17, 1, 1, 3] @@ -106,10 +102,10 @@ def toBytes(bytestring): >>> toBytes("3B6500 009C1101 0103") [59, 101, 0, 0, 156, 17, 1, 1, 3] """ - packedstring = bytestring.replace(' ', '').replace(' ','').replace('\n', '') + try: - return list(map(lambda x: int(''.join(x), 16), zip(*[iter(packedstring)] * 2))) - except (KeyError, ValueError): + return list(bytes.fromhex(bytestring)) + except ValueError: raise TypeError('not a string representing a list of bytes') @@ -156,7 +152,7 @@ def toBytes(bytestring): } -def toGSM3_38Bytes(stringtoconvert): +def toGSM3_38Bytes(stringtoconvert: str | bytes) -> list[int]: """Returns a list of bytes from a string using GSM 3.38 conversion table. @param stringtoconvert: string to convert @@ -171,19 +167,17 @@ def toGSM3_38Bytes(stringtoconvert): result = [] for char in stringtoconvert: - if ((char >= "%") and (char <= "?")): - result.append(ord(char)) - elif ((char >= "A") and (char <= "Z")): - result.append(ord(char)) - elif ((char >= "a") and (char <= "z")): + if ("%" <= char <= "?") or ("A" <= char <= "Z") or ("a" <= char <= "z"): result.append(ord(char)) else: result.append(__dic_GSM_3_38__[char]) return result -def toHexString(data=[], format=0): - """Returns an hex string representing bytes +def toHexString(data: list[int] | None = None, format: int = 0) -> str: + """Convert a list of integers to a formatted string of hexadecimal. + + Integers larger than 255 will be truncated to two-byte hexadecimal pairs. @param data: a list of bytes to stringify, e.g. [59, 22, 148, 32, 2, 1, 0, 0, 13] @@ -210,61 +204,53 @@ def toHexString(data=[], format=0): '0X3B, 0X65, 0X00, 0X00, 0X9C, 0X11, 0X01, 0X01, 0X03' """ - for byte in tuple(data): - pass - - if type(data) is not list: + if not (data is None or isinstance(data, list)): raise TypeError('not a list of bytes') - if data is None or data == []: + if not data: return "" - else: - pformat = "%-0.2X" - if COMMA & format: - separator = "," + + pformat = "%-0.2X" + separator = "" + if COMMA & format: + separator = "," + if not PACK & format: + separator += " " + if HEX & format: + if UPPERCASE & format: + pformat = "0X" + pformat else: - separator = "" - if not PACK & format: - separator = separator + " " - if HEX & format: - if UPPERCASE & format: - pformat = "0X" + pformat - else: - pformat = "0x" + pformat - return (separator.join(map(lambda a: pformat % ((a + 256) % 256), data))).rstrip() - - -# FIXME This appears to duplicate toASCIIString() -def HexListToBinString(hexlist): - """ + pformat = "0x" + pformat + return separator.join(pformat % (a & 0xff) for a in data).rstrip() + + +def HexListToBinString(hexlist: list[int]) -> str: + """Deprecated. Use `bytes(hexlist).decode("utf-8")` or similar. + >>> HexListToBinString([78, 117, 109, 98, 101, 114, 32, 49, 48, 49]) 'Number 101' """ - return ''.join(map(chr, hexlist)) - -# FIXME This appears to duplicate to ASCIIBytes() -def BinStringToHexList(binstring): - """ - >>> BinStringToHexList("Number 101") - [78, 117, 109, 98, 101, 114, 32, 49, 48, 49] - """ - return list(map(ord, binstring)) + warnings.warn( + 'Use `bytes(hexlist).decode("utf-8")` or similar.', + DeprecationWarning, + ) + return bytes(hexlist).decode("utf-8") -def hl2bs(hexlist): - """An alias for HexListToBinString +def BinStringToHexList(binstring: str) -> list[int]: + """Deprecated. Use `list(binstring.encode("utf-8"))` or similar. - >>> hl2bs([78, 117, 109, 98, 101, 114, 32, 49, 48, 49]) - 'Number 101' + >>> BinStringToHexList("Number 101") + [78, 117, 109, 98, 101, 114, 32, 49, 48, 49] """ - return HexListToBinString(hexlist) + warnings.warn( + 'Use `list(binstring.encode("utf-8"))` or similar.', + DeprecationWarning, + ) + return list(binstring.encode("utf-8")) -def bs2hl(binstring): - """An alias for BinStringToHexList - >>> bs2hl("Number 101") - [78, 117, 109, 98, 101, 114, 32, 49, 48, 49] - """ - return BinStringToHexList(binstring) +hl2bs = HexListToBinString +bs2hl = BinStringToHexList diff --git a/test/test_util.py b/test/test_util.py index 41936fcc..585c04c5 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -126,22 +126,26 @@ def test_toHexString(self): def test_HexListToBinString(self): data_in = [1, 2, 3] data_out = "\x01\x02\x03" - self.assertEqual(HexListToBinString(data_in), data_out) + with self.assertWarns(DeprecationWarning): + self.assertEqual(HexListToBinString(data_in), data_out) def test_BinStringToHexList(self): data_in = "\x01\x02\x03" data_out = [1, 2, 3] - self.assertEqual(BinStringToHexList(data_in), data_out) + with self.assertWarns(DeprecationWarning): + self.assertEqual(BinStringToHexList(data_in), data_out) def test_hl2bs(self): data_in = [78, 117, 109, 98, 101, 114, 32, 49, 48, 49] data_out = 'Number 101' - self.assertEqual(hl2bs(data_in), data_out) + with self.assertWarns(DeprecationWarning): + self.assertEqual(hl2bs(data_in), data_out) def test_bs2hl(self): data_in = 'Number 101' data_out = [78, 117, 109, 98, 101, 114, 32, 49, 48, 49] - self.assertEqual(bs2hl(data_in), data_out) + with self.assertWarns(DeprecationWarning): + self.assertEqual(bs2hl(data_in), data_out) if __name__ == '__main__': From d71d968666786fcb7adb1e7c502541230c4a8ca0 Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Wed, 25 Sep 2024 11:26:45 -0500 Subject: [PATCH 2/2] Feedback: Update references to deprecated functions This commit also updates an example for SHA-1 and MD5 hashing. --- src/smartcard/doc/user-guide.rst | 38 ++++++++++++-------------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/src/smartcard/doc/user-guide.rst b/src/smartcard/doc/user-guide.rst index 11c037e3..a59bbb9b 100644 --- a/src/smartcard/doc/user-guide.rst +++ b/src/smartcard/doc/user-guide.rst @@ -1408,39 +1408,29 @@ applications. Binary strings and list of bytes ================================ -pycrypto processes binary strings, i.e. Python strings that contains -characters such as '\01\42\70\23', whereas pyscard processes APDUs as -list of bytes such as [0x01, 0x42, 0x70, 0x23]. The utility function -HexListToBinString and BinStringToHexList (and their short name versions -hl2bs and bs2hl) provide conversion between the two types. +Cryptography packages frequently process ``bytes`` objects like ``b"\x01\x42\x70\x23"``, +whereas pyscard processes APDUs as list of integers, such as ``[0x01, 0x42, 0x70, 0x23]``. -.. sourcecode:: python +It's possible to convert between these objects like this: - from smartcard.util import HexListToBinString, BinStringToHexList +.. sourcecode:: python test_data = [0x01, 0x42, 0x70, 0x23] - binstring = HexListToBinString(test_data) - hexlist = BinStringToHexList(binstring) - print(binstring, hexlist) + bytes_object = bytes(test_data) + list_of_ints = list(bytes_object) + print(bytes_object, list_of_ints) -pycrypto supports the following hashing algorithms: SHA-1, MD2, MD4 et -MD5. To hash 16 bytes of data with SHA-1: +To hash several bytes of data with SHA-1: .. sourcecode:: python - from Crypto.Hash import SHA - - from smartcard.util import toHexString, PACK + import hashlib test_data = [0x01, 0x42, 0x70, 0x23] - binstring = HexListToBinString(test_data) - - zhash = SHA.new(binstring) - hash_as_string = zhash.digest()[:16] - hash_as_bytes = BinStringToHexList(hash_as_string) - print(hash_as_string, ',', toHexString(hash_as_bytes, PACK)) + digest = hashlib.sha1(bytes(test_data)).hexdigest() + print(digest) -To perform MD5 hashing, just replace SHA by MD5 in the previous script. +To perform MD5 hashing, just replace the ``.sha1()`` function with ``.md5()`` in the previous script. Secret key cryptography ======================= @@ -1456,11 +1446,11 @@ mode: from smartcard.util import toBytes key = "31323334353637383132333435363738" - key_as_binstring = HexListToBinString(toBytes(key)) + key_as_binstring = bytes(toBytes(key)) zdes = DES3.new(key_as_binstring, DES3.MODE_ECB) message = "71727374757677787172737475767778" - message_as_binstring = HexListToBinString(toBytes(message)) + message_as_binstring = bytes(toBytes(message))) encrypted_as_string = zdes.encrypt(message_as_binstring) decrypted_as_string = zdes.decrypt(encrypted_as_string)