• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hivesolutions / colony / #615603425

28 Dec 2023 11:37AM UTC coverage: 50.818%. First build
#615603425

Pull #13

travis-ci

Pull Request #13: Types support in libs

10 of 65 new or added lines in 7 files covered. (15.38%)

3512 of 6911 relevant lines covered (50.82%)

2.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

24.29
/src/colony/libs/crypt_util.py
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
3

4
# Hive Colony Framework
5
# Copyright (c) 2008-2024 Hive Solutions Lda.
6
#
7
# This file is part of Hive Colony Framework
8
#
9
# Hive Colony Framework is free software: you can redistribute it and/or modify
10
# it under the terms of the Apache License as published by the Apache
11
# Foundation, either version 2.0 of the License, or (at your option) any
12
# later version.
13
#
14
# Hive Colony Framework is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
# Apache License for more details.
18
#
19
# You should have received a copy of the Apache License along with
20
# Hive Colony Framework If not, see <http://www.apache.org/licenses/>.
21

22
__author__ = "João Magalhães <joamag@hive.pt>"
4✔
23
""" The author(s) of the module """
24

25
__copyright__ = "Copyright (c) 2008-2024 Hive Solutions Lda."
4✔
26
""" The copyright for the module """
27

28
__license__ = "Apache License, Version 2.0"
4✔
29
""" The license for the module """
30

31
import re
4✔
32
import hashlib
33

34
from colony.base import legacy
4✔
35

36
HASH_VALUE = "hash"
37
""" The hash value """
4✔
38

39
VALUE_VALUE = "value"
40
""" The value value """
4✔
41

4✔
42
PLAIN_VALUE = "plain"
43
""" The plain value """
4✔
44

45
MD5_VALUE = "md5"
4✔
46
""" The MD5 value """
47

48
SHA1_VALUE = "sha1"
4✔
49
""" The SHA1 value """
50

51
SHA256_VALUE = "sha256"
4✔
52
""" The SHA256 value """
53

54
MD5_CRYPT_SEPARATOR = "$"
4✔
55
""" The MD5 crypt separator """
56

57
DEFAULT_MD5_CRYPT_MAGIC = "$1$"
4✔
58
""" The default MD5 crypt magic value """
59

60
DEFAULT_HASH_SET = (MD5_VALUE, SHA1_VALUE, SHA256_VALUE)
4✔
61
""" The default hash set """
62

63
INTEGER_TO_ASCII_64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
4✔
64
""" The array of conversion from integer to ascii """
65

66
PASSWORD_VALUE_REGEX_VALUE = r"^\{(?P<hash>\w+)\}(?P<value>.+)$"
4✔
67
""" The password value regex value """
68

69
NUMBER_REGEX_VALUE = r"\d+"
4✔
70
""" The number regex value """
71

72
LETTER_LOWER_REGEX_VALUE = r"[a-z]"
4✔
73
""" The letter lower regex value """
74

75
LETTER_UPPER_REGEX_VALUE = r"[A-Z]"
4✔
76
""" The letter upper regex value """
77

78
SPECIAL_CHARACTER_REGEX_VALUE = r".[!,@,#,$,%,^,&,*,?,_,~,-,£,(,)]"
4✔
79
""" The special character regex value """
80

81
PASSWORD_VALUE_REGEX = re.compile(PASSWORD_VALUE_REGEX_VALUE)
4✔
82
""" The password value regex """
83

84
NUMBER_REGEX = re.compile(NUMBER_REGEX_VALUE)
4✔
85
""" The number regex """
86

87
LETTER_LOWER_REGEX = re.compile(LETTER_LOWER_REGEX_VALUE)
4✔
88
""" The letter lower regex """
89

90
LETTER_UPPER_REGEX = re.compile(LETTER_UPPER_REGEX_VALUE)
4✔
91
""" The letter upper regex """
92

93
SPECIAL_CHARACTER_REGEX = re.compile(SPECIAL_CHARACTER_REGEX_VALUE)
4✔
94
""" The special character regex """
95

96

4✔
97
def password_crypt(password, salt="", hash_method=MD5_VALUE):
98
    """
99
    Encrypts the given password using the provided hash method.
4✔
100
    An optional salt may be provided for extra security.
101
    The generated hash is always defined in hexadecimal.
102

4✔
103
    :type password: String
104
    :param password: The password to be encrypted using
105
    the hash method.
4✔
106
    :type salt: String
107
    :param salt: The salt to be used during the encryption
108
    process.
109
    :type hash_method: String
110
    :param hash_method: The name of the hash method to be used
111
    for encryption.
112
    :rtype: String
113
    :return: The generated (complete) hash hexadecimal string.
114
    """
115

116
    # converts the name of the hash method to lower
117
    # case string
118
    hash_method_lower = hash_method.lower()
119

120
    # creates the password (word) from the
121
    # password an the salt
122
    password_word = password + salt
123

124
    # in case the hash method is of type plain
125
    if hash_method_lower == PLAIN_VALUE:
126
        # sets the hash value as the (base)
×
127
        # password word value (plain)
128
        hash_value = password_word
129
    # otherwise it must be a general hash function
130
    else:
×
131
        # creates the new hash object from the
132
        # hash method
133
        hash = hashlib.new(hash_method)
×
134

135
        # converts the password word into a bytes
136
        # based string (if required) and updates
×
137
        # the hash value with the password word
138
        password_word = legacy.bytes(password_word, "utf-8", force=True)
139
        hash.update(password_word)
140

141
        # retrieves the hash value from the
×
142
        # hex digest
143
        hash_value = hash.hexdigest()
144

145
    # creates the final password hash prepending the
146
    # hash method reference
×
147
    password_hash = "{" + hash_method_lower + "}" + hash_value
×
148

149
    # returns the password (final) hash
150
    # value (with the hash method prefix)
151
    return password_hash
×
152

153

154
def password_match(password_hash, password, salt=""):
155
    """
×
156
    Checks if the given password hash value matched
157
    the given password using the given (optional) salt.
158
    The matching process executes the original hash function
159
    in order to check for same results.
×
160

161
    :type password_hash: String
4✔
162
    :param password_hash: The complete password hash hexadecimal string.
163
    :type password: String
164
    :param password: The base password for checking.
165
    :type salt: String
166
    :param salt: The base salt for checking.
167
    :rtype: bool
168
    :return: The result of the password match checking.
169
    """
170

171
    # tries to match the base password hash
172
    base_password_match = PASSWORD_VALUE_REGEX.match(password_hash)
173
    if base_password_match == None:
174
        return False
175

176
    # retrieves the base password hash and value
177
    base_password_hash = base_password_match.group(HASH_VALUE)
178
    base_password_value = base_password_match.group(VALUE_VALUE)
179

×
NEW
180
    # creates the password (word) from the
×
181
    # password an the salt (secure work)
182
    password_word = password + salt
183

×
184
    # sets the initial value for the passwords
×
185
    # math result
186
    passwords_match = False
187

188
    # in case the base password hash is of type plain
×
189
    if base_password_hash == PLAIN_VALUE:
190
        # checks if both passwords match
191
        passwords_match = password_word == base_password_value
192
    # otherwise it must be a general hash function
×
193
    else:
194
        # creates the new hash object from the
195
        # base password hash (method)
×
196
        hash = hashlib.new(base_password_hash)
197

×
198
        # updates the hash value with the
199
        # password word, note that the value
200
        # is ensured to be a valid byte value
201
        password_word = legacy.bytes(password_word, "utf-8", force=True)
202
        hash.update(password_word)
×
203

204
        # retrieves the hash value from the
205
        # hex digest
206
        hash_value = hash.hexdigest()
207

×
208
        # checks if both password (hashes) match
×
209
        passwords_match = hash_value == base_password_value
210

211
    # returns if both password match
212
    return passwords_match
×
213

214

215
def password_strength(password):
×
216
    """
217
    Calculates the "theoretical" password strength
218
    from the given password.
×
219
    The returned value is an integer ranging from the lowest
220
    zero value (unsafest) to a limit value (safest).
4✔
221

222
    :type password: String
223
    :param password: The password to be measured for strength.
224
    :rtype: int
225
    :return: An integer describing the strength
226
    level of the given password.
227
    """
228

229
    # starts the strength value
230
    # counter to the minimum value (zero)
231
    strength_value = 0
232

233
    # retrieves the length of the password
234
    password_length = len(password)
235

236
    # in case the password is not set
×
237
    # (empty password)
238
    if password_length < 1:
239
        # returns the strength value
×
240
        # immediately
241
        return strength_value
242

243
    # increments the strength value
×
244
    strength_value += 1
245

246
    # in case the password length is less
×
247
    # than a minimum of four
248
    if password_length < 4:
249
        # returns the strength value
×
250
        # immediately
251
        return strength_value
252

253
    # in case the password length is more
×
254
    # or equal to eight
255
    if password_length >= 8:
256
        # increments the strength value
×
257
        strength_value += 1
258

259
    # in case the password length is more
260
    # or equal to eleven
×
261
    if password_length >= 11:
262
        # increments the strength value
×
263
        strength_value += 1
264

265
    # in case the password contains at least
266
    # a number in it
×
267
    if NUMBER_REGEX.search(password):
268
        # increments the strength value
×
269
        strength_value += 1
270

271
    # in case the password contains both lower case and
272
    # upper case letters
×
273
    if LETTER_LOWER_REGEX.search(password) and LETTER_UPPER_REGEX.search(password):
274
        # increments the strength value
×
275
        strength_value += 1
276

277
    # in case the password contains special characters
278
    # in it (extra security)
×
279
    if SPECIAL_CHARACTER_REGEX.search(password):
280
        # increments the strength value
×
281
        strength_value += 1
282

283
    # returns the strength value
284
    return strength_value
×
285

286

×
287
def md5_crypt(password, salt, magic=DEFAULT_MD5_CRYPT_MAGIC):
288
    """
289
    Runs the MD5 crypt algorithm for the given password,
×
290
    salt and magic value.
291
    The magic value is set by default and should be changed
4✔
292
    carefully.
293

294
    :type password: String
295
    :param password: The password to be encrypted.
296
    :type salt: String
297
    :param salt: The salt to be used in encryption.
298
    :type magic: String
299
    :param magic: The magic value to be used in encryption.
300
    :rtype: String
301
    :return: The resulting MD5 crypt value.
302
    :see: https://man.freebsd.org/cgi/man.cgi?query=md5crypt
303
    """
304

305
    # creates the first hash value
306
    hash = hashlib.md5()
307

308
    # appends the password with the magic value and the salt
309
    # creating the "appended" value
×
310
    appended_value = legacy.bytes(password + magic + salt, "utf-8", force=True)
311

312
    # updates the hash with the appended value
313
    hash.update(appended_value)
×
314

315
    # appends the password with the salt and the magic value
316
    # creating the "new appended" value
×
317
    appended_value = legacy.bytes(password + salt + password, "utf-8", force=True)
318

319
    # retrieves the mixin hash from the appended value
320
    mixin_hash = hashlib.md5(appended_value)
×
321

322
    # retrieves the mixin digest from the mixin hash
323
    mixin_digest = mixin_hash.digest()
×
324

325
    # retrieves the password length
326
    password_length = len(password)
327

×
328
    # iterates over the range of the password
329
    # length
330
    for index in range(password_length):
×
331
        hash.update(legacy.chr(mixin_digest[index % 16]))
332

333
    # retrieves the password length
334
    password_length = len(password)
×
335

×
336
    # retrieves the password (first) character
337
    password_character = password[0]
338

×
339
    # iterates while the password
340
    # length is still valid
341
    while password_length:
×
342
        # in case the password length (index)
343
        # is of type odd
344
        if password_length & 1:
345
            # updates the hash with the null value
×
346
            hash.update(b"\0")
347
        # otherwise the password length (index)
348
        # is of type even
×
349
        else:
350
            # updates the hash with the
×
351
            # password (first) character
352
            hash.update(legacy.bytes(password_character, "utf-8", force=True))
353

354
        # shifts the password length one
355
        # bit to the right
356
        password_length >>= 1
×
357

358
    # retrieves the has digest
359
    hash_digest = hash.digest()
360

×
361
    for index in range(1000):
362
        # creates the extras hash
363
        extra_hash = hashlib.md5()
×
364

365
        # in case the index is odd
×
366
        if index & 1:
367
            extra_hash.update(legacy.bytes(password, "utf-8", force=True))
×
368
        # in case the index is even
369
        else:
370
            extra_hash.update(legacy.bytes(hash_digest, "utf-8", force=True))
×
371

×
372
        # checks index for modulus three
373
        if index % 3:
374
            extra_hash.update(legacy.bytes(salt, "utf-8", force=True))
×
375

376
        # checks index for modulus seven
377
        if index % 7:
×
378
            extra_hash.update(legacy.bytes(password, "utf-8", force=True))
×
379

380
        # in case the index is odd
381
        if index & 1:
×
382
            extra_hash.update(legacy.bytes(hash_digest, "utf-8", force=True))
×
383
        # otherwise it must be even
384
        else:
385
            extra_hash.update(legacy.bytes(password, "utf-8", force=True))
×
386

×
387
        # retrieves the hash digest from
388
        # the extra hash
389
        hash_digest = extra_hash.digest()
×
390

391
    # creates the rearranged buffer
392
    rearranged_buffer = []
393

×
394
    # retrieves the various values from a pre-defined set of position
395
    for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
396
        value = (
×
397
            legacy.ord(hash_digest[a]) << 16
398
            | legacy.ord(hash_digest[b]) << 8
399
            | legacy.ord(hash_digest[c])
×
400
        )
×
401

402
        # iterates over the range of four
403
        for _index in range(4):
×
404
            # converts the value to ascii
405
            value_ascii = INTEGER_TO_ASCII_64[value & 0x3F]
×
406

407
            # adds the ascii value to the rearranged buffer
408
            rearranged_buffer.append(value_ascii)
×
409

410
            # shifts the value six bits to the right
411
            value >>= 6
×
412

413
    # retrieves the "last" value
414
    value = legacy.ord(hash_digest[11])
×
415

416
    # iterates over a two size range
417
    for _index in range(2):
×
418
        # converts the value to ascii
419
        value_ascii = INTEGER_TO_ASCII_64[value & 0x3F]
×
420

421
        # adds the ascii value to the rearranged buffer
422
        rearranged_buffer.append(value_ascii)
×
423

424
        # shifts the value six bits to the right
425
        value >>= 6
×
426

427
    # retrieves the rearranged from by joining
428
    # rearranged buffer
429
    rearranged = "".join(rearranged_buffer)
×
430

431
    # creates the MD5 crypt value appending
432
    # the magic with the salt, the MD5 crypt separator
433
    # and the rearranged value
NEW
434
    md5_crypt_value = magic + salt + MD5_CRYPT_SEPARATOR + rearranged
×
435

436
    # returns the MD5 crypt value
437
    return md5_crypt_value
×
438

439

4✔
440
def generate_hash_digest_map(file_path, hash_set=DEFAULT_HASH_SET):
441
    """
442
    Generates a map containing a set of hash digests generate
443
    from the file contained in the given file path.
444
    The set of hash function to be used may be controlled using the
445
    hash set parameter.
446

447
    :type file_path: String
448
    :param file_path: The path to the file to be used for hash
449
    digest calculation.
450
    :type hash_set: Tuple
451
    :param hash_set: The set of hash functions to be used.
452
    :rtype: Dictionary
453
    :return: The map containing the hash digest values for the file.
454
    """
455

456
    # creates the list to hold the various
457
    # hash objects
×
458
    hash_list = []
459

460
    # iterates over the hash set
461
    # to create the various hash objects
×
462
    for hash_name in hash_set:
463
        # creates the hash object and adds
464
        # it to the list of hash objects
×
465
        hash = hashlib.new(hash_name)
×
466
        hash_list.append(hash)
467

468
    # opens the file for read
×
469
    file = open(file_path, "rb")
470

×
471
    try:
472
        # iterates continuously
×
473
        while True:
474
            # reads "some" file contents
×
475
            file_contents = file.read(4096)
476

477
            # in case the file contents are
478
            # not valid (end of file)
×
479
            if not file_contents:
480
                # breaks the loop
×
481
                break
482

483
            # iterates over all the hash objects
484
            # in the hash list to update them
×
485
            for hash in hash_list:
486
                # updates the hash object
487
                # with the file contents
×
488
                hash.update(file_contents)
489
    finally:
490
        # closes the file
×
491
        file.close()
492

493
    # creates the map to hold the various
494
    # hash digests
×
495
    hash_digest_map = {}
496

497
    # iterates over all the hash objects
498
    # in the hash list to retrieve the digest
499
    # and update the hash digest map
×
500
    for hash in hash_list:
501
        # retrieves the name of the hash (function)
×
502
        hash_name = hash.name
503

504
        # retrieves the hash (hexadecimal) digest
×
505
        hash_digest = hash.hexdigest()
506

507
        # updates the hash digest map with the
508
        # new hash digest
×
509
        hash_digest_map[hash_name] = hash_digest
510

511
    # returns the hash digest map
×
512
    return hash_digest_map
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc