Coverage for /var/devmt/py/utils4_1.8.0/utils4/crypto.py: 100%

84 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2025-08-26 21:39 +0100

1#!/usr/bin/env python3 

2# -*- coding: utf-8 -*- 

3""" 

4:Purpose: This module provides a light wrapper around the ``base64`` 

5 and ``hashlib`` libraries to provide some additional 

6 functionality. 

7 

8:Developer: J Berendt 

9:Email: support@s3dev.uk 

10 

11:Comments: n/a 

12 

13:Example: 

14 Example code use:: 

15 

16 >>> from utils4.crypto import crypto 

17 

18 

19 To obtain a quick MD5 hash:: 

20 

21 >>> s = "The quick brown fox jumps over the lazy dog" 

22 >>> output = crypto.md5(s) 

23 >>> print(output) 

24 

25 9e107d9d372bb6826bd81d3542a419d6 

26 

27 

28 To obtain a Base64 encoded MD5 hash:: 

29 

30 >>> s = "The quick brown fox jumps over the lazy dog" 

31 >>> output = crypto.b64md5(s) 

32 >>> print(output) 

33 

34 OWUxMDdkOWQzNzJiYjY4MjZiZDgxZDM1NDJhNDE5ZDY= 

35 

36 

37 For examples on checksumming a file, please refer to: 

38 

39 - :meth:`Crypto.checksum_crc32` 

40 - :meth:`Crypto.checksum_md5` 

41 - :meth:`Crypto.checksum_sha1` 

42 - :meth:`Crypto.checksum_sha256` 

43 - :meth:`Crypto.checksum_sha512` 

44 

45""" 

46# pylint: disable=invalid-name 

47 

48import base64 

49import hashlib 

50import zlib 

51from typing import Union 

52from utils4 import convert 

53 

54 

55class Crypto: 

56 """Main class used for hashing and encoding. 

57 

58 This class acts as a simple wrapper around the ``base64`` and 

59 ``hashlib`` libraries, providing additional functionality. 

60 

61 """ 

62 

63 def b64(self, data: str, decode: bool=True) -> Union[bytes, str]: 

64 """Create an encoded or decoded Base64 encryption. 

65 

66 Args: 

67 data (str): String to be encoded. If a ``str`` data type is 

68 received, it is encoded to ``bytes`` before encoding. 

69 decode (bool, optional): Return a decoded string. Defaults to True. 

70 

71 Returns: 

72 Union[bytes, str]: An encoded or decoded Base64 encrypted string. 

73 

74 """ 

75 data = self._encode(data) 

76 b = base64.b64encode(data) 

77 if decode: 

78 b = b.decode() 

79 return b 

80 

81 def b64md5(self, data: Union[iter, str], trunc: int=None) -> str: 

82 """Create an optionally truncated Base64 encoded MD5 hash from a 

83 string or array. 

84 

85 Args: 

86 data (Union[iter, str]): A string or an iterable object containing 

87 strings to be encoded. 

88 trunc (int, optional): Truncate the Base64 string to (n) 

89 characters. As string slicing is used, values such as ``-1`` 

90 are also valid. Defaults to None. 

91 

92 Returns: 

93 str: An (optionally truncated) Base64 encoded MD5 hash of the 

94 passed string or iterable. 

95 

96 """ 

97 s = ''.join(data).encode() 

98 h = self.md5(s, decode=False) 

99 b = self.b64(h, decode=True) 

100 b = b[:trunc] if trunc else b 

101 return b 

102 

103 @staticmethod 

104 def checksum_crc32(path: str, return_integer: bool=False) -> Union[int, str]: 

105 """Generate a 32-bit CRC32 checksum for the given file. 

106 

107 Args: 

108 path (str): Full path to the file. 

109 return_integer (bool, optional): Return the original unsigned 

110 32-bit integer, rather than the hex string. Defaults to False. 

111 

112 Important: 

113 This algorithm is *not* cryptographically strong and should not be 

114 used for authentication or digital signatures; nor is it suitable 

115 for use as a general hash algorithm. 

116 

117 -- zlib.crc32 `Documentation`_ 

118 

119 .. _Documentation: https://docs.python.org/3/library/zlib.html#zlib.crc32 

120 

121 :Design: 

122 This method breaks the file into 32768-byte chunks for more memory 

123 efficient reading. Meaning this method has a maximum memory use 

124 overhead of ~32K. 

125 

126 :Example: 

127 

128 Example for calculating the crc32 checksum for a file, returning a 

129 hex string:: 

130 

131 >>> from utils4.crypto import crypto 

132 

133 >>> crypto.checksum_crc32(path='/tmp/test.txt') 

134 '2a30e66b' 

135 

136 

137 Example for calculating the crc32 checksum for a file, returning 

138 an integer:: 

139 

140 >>> from utils4.crypto import crypto 

141 

142 >>> crypto.checksum_crc32(path='/tmp/test.txt', return_integer=True) 

143 707847787 

144 

145 Returns: 

146 Union[int, str]: If the ``return_integer`` value is ``False`` 

147 (default action), a CRC32 32-bit hex string (checksum string) of 

148 the file's contents is returned. Otherwise, an unsigned 32-bit 

149 integer is returned. 

150 

151 """ 

152 size = 1024*32 # 32K chunks 

153 with open(path, 'rb') as f: 

154 crcval = 0 

155 chunk = f.read(size) 

156 while len(chunk) > 0: 

157 crcval = zlib.crc32(chunk, crcval) 

158 chunk = f.read(size) 

159 if return_integer: 

160 rtn = crcval 

161 else: 

162 rtn = convert.int2hex(crcval & 0xFFFFFFFF) 

163 return rtn 

164 

165 @staticmethod 

166 def checksum_md5(path: str) -> str: 

167 """Generate a 128-bit MD5 checksum for the given file. 

168 

169 Args: 

170 path (str): Full path to the file. 

171 

172 :Design: 

173 This method breaks the file into 32768-byte chunks 

174 (64-bytes * 512 blocks) for more memory efficient reading; 

175 taking advantage of the fact that MD5 uses 512-bit (64-byte) digest 

176 blocks. Meaning this method has a maximum memory use overhead of 

177 ~32K. 

178 

179 :Example: 

180 

181 Example calculating the MD5 checksum for a file:: 

182 

183 >>> from utils4.crypto import crypto 

184 

185 >>> crypto.checksum_md5(path='/tmp/test.txt') 

186 '9ec06901e8f25eb9810c5e0db88e7dcd' 

187 

188 Returns: 

189 str: A 128-bit MD5 hex digest (checksum string) of the file's 

190 contents. 

191 

192 """ 

193 md5 = hashlib.md5() 

194 size = 64*512 # 32K chunks - 64-byte digest blocks (x512 blocks) 

195 with open(path, 'rb') as f: 

196 chunk = f.read(size) 

197 while len(chunk) > 0: 

198 md5.update(chunk) 

199 chunk = f.read(size) 

200 return md5.hexdigest() 

201 

202 @staticmethod 

203 def checksum_sha1(path: str) -> str: 

204 """Generate a 160-bit SHA1 checksum for the given file. 

205 

206 Args: 

207 path (str): Full path to the file. 

208 

209 :Design: 

210 This method breaks the file into 32768-byte chunks 

211 (64-bytes * 512 blocks) for more memory efficient reading; 

212 taking advantage of the fact that SHA1 uses 512-bit (64-byte) 

213 digest blocks. Meaning this method has a maximum memory use 

214 overhead of ~32K. 

215 

216 :Example: 

217 

218 Example calculating the SHA1 checksum for a file:: 

219 

220 >>> from utils4.crypto import crypto 

221 

222 >>> crypto.checksum_sha1(path='/tmp/test.txt') 

223 'e49a1493c637a24800119fb53ef7dbc580221e89' 

224 

225 Returns: 

226 str: A 160-bit SHA1 hex digest (checksum string) of the file's 

227 contents. 

228 

229 """ 

230 sha1 = hashlib.sha1() 

231 size = 64*512 # 32K chunks - 64-byte digest blocks (x512 blocks) 

232 with open(path, 'rb') as f: 

233 chunk = f.read(size) 

234 while len(chunk) > 0: 

235 sha1.update(chunk) 

236 chunk = f.read(size) 

237 return sha1.hexdigest() 

238 

239 @staticmethod 

240 def checksum_sha256(path: str) -> str: 

241 """Generate a 256-bit SHA256 checksum for the given file. 

242 

243 Args: 

244 path (str): Full path to the file. 

245 

246 :Design: 

247 This method breaks the file into 32768-byte chunks 

248 (64-bytes * 512 blocks) for more memory efficient reading; 

249 taking advantage of the fact that SHA256 uses 512-bit (64-byte) 

250 digest blocks. Meaning this method has a maximum memory use 

251 overhead of ~32K. 

252 

253 :Example: 

254 

255 Example calculating the SHA256 checksum for a file:: 

256 

257 >>> from utils4.crypto import crypto 

258 

259 >>> crypto.checksum_sha256(path='/tmp/test.txt') 

260 'e899df8e51b60bf8a6ede73fe5c7b4267bf5e48937e848bac3c6efd906833821' 

261 

262 Returns: 

263 str: A 256-bit SHA256 hex digest (checksum string) of the file's 

264 contents. 

265 

266 """ 

267 sha256 = hashlib.sha256() 

268 size = 64*512 # 32K chunks - 64-byte digest blocks (x512 blocks) 

269 with open(path, 'rb') as f: 

270 chunk = f.read(size) 

271 while len(chunk) > 0: 

272 sha256.update(chunk) 

273 chunk = f.read(size) 

274 return sha256.hexdigest() 

275 

276 @staticmethod 

277 def checksum_sha512(path: str) -> str: 

278 """Generate a 512-bit SHA512 checksum for the given file. 

279 

280 Args: 

281 path (str): Full path to the file. 

282 

283 :Design: 

284 This method breaks the file into 32768-byte chunks 

285 (128-bytes * 256 blocks) for more memory efficient reading; 

286 taking advantage of the fact that SHA512 uses 1024-bit (128-byte) 

287 digest blocks. Meaning this method has a maximum memory use 

288 overhead of ~32K. 

289 

290 :Example: 

291 

292 Example calculating the SHA512 checksum for a file:: 

293 

294 >>> from utils4.crypto import crypto 

295 

296 >>> crypto.checksum_sha512(path='/tmp/test.txt') 

297 ('247adcb6f5b284b3e45c9281171ba7a6' 

298 '2502692ee9ee8020bd5827602972409f' 

299 '9bdfc2ec7e5452223c19b3745d3f04e2' 

300 '542ef0d0e075139d1ee3b5f678c9aaec') # Single string 

301 

302 Returns: 

303 str: A 512-bit SHA512 hex digest (checksum string) of the file's 

304 contents. 

305 

306 """ 

307 sha512 = hashlib.sha512() 

308 size = 128*256 # 32K chunks - 128-byte digest blocks (x256 blocks) 

309 with open(path, 'rb') as f: 

310 chunk = f.read(size) 

311 while len(chunk) > 0: 

312 sha512.update(chunk) 

313 chunk = f.read(size) 

314 return sha512.hexdigest() 

315 

316 def md5(self, data: str, decode: bool=True) -> str: 

317 """Create an optionally encoded or decoded MD5 hash. 

318 

319 Args: 

320 data (str): String to be hashed. If a ``str`` data type 

321 is passed, it is encoded to ``bytes`` before hashing. 

322 decode (bool, optional): Return a decoded string. Defaults to True. 

323 

324 Returns: 

325 str: An encoded or decoded MD5 hash, depending on the value passed 

326 to the ``decode`` parameter. 

327 

328 """ 

329 data = self._encode(data) 

330 h = hashlib.md5(data).hexdigest() 

331 if not decode: 

332 h = h.encode() 

333 return h 

334 

335 @staticmethod 

336 def _encode(data: Union[bytes, str]) -> bytes: 

337 """Test if a string is ``str`` or ``bytes`` before processing. 

338 

339 Args: 

340 data (Union[bytes, str]): String value to be encoded. 

341 

342 If the received ``data`` parameter is a ``str`` type, it is converted 

343 to a ``bytes`` type and returned. If the string is already a ``bytes`` 

344 type, it is returned, unmodified. 

345 

346 Raises: 

347 ValueError: If the ``data`` object is neither a ``str`` or 

348 ``bytes`` type. 

349 

350 Returns: 

351 bytes: A ``bytes`` encoded string. 

352 

353 """ 

354 if not isinstance(data, (bytes, str)): 

355 raise ValueError('Expected a bytes or str type.') 

356 data = data.encode() if isinstance(data, str) else data 

357 return data 

358 

359 

360crypto = Crypto()