1"""
2A collection of hashing and encoding utils.
3"""
4
5import base64
6import hashlib
7import hmac
8import os
9import random
10
11import salt.utils.files
12import salt.utils.platform
13import salt.utils.stringutils
14from salt.utils.decorators.jinja import jinja_filter
15
16
17@jinja_filter("base64_encode")
18def base64_b64encode(instr):
19    """
20    Encode a string as base64 using the "modern" Python interface.
21
22    Among other possible differences, the "modern" encoder does not include
23    newline ('\\n') characters in the encoded output.
24    """
25    return salt.utils.stringutils.to_unicode(
26        base64.b64encode(salt.utils.stringutils.to_bytes(instr)),
27        encoding="utf8" if salt.utils.platform.is_windows() else None,
28    )
29
30
31@jinja_filter("base64_decode")
32def base64_b64decode(instr):
33    """
34    Decode a base64-encoded string using the "modern" Python interface.
35    """
36    decoded = base64.b64decode(salt.utils.stringutils.to_bytes(instr))
37    try:
38        return salt.utils.stringutils.to_unicode(
39            decoded, encoding="utf8" if salt.utils.platform.is_windows() else None
40        )
41    except UnicodeDecodeError:
42        return decoded
43
44
45def base64_encodestring(instr):
46    """
47    Encode a byte-like object as base64 using the "modern" Python interface.
48
49    Among other possible differences, the "modern" encoder includes
50    a newline ('\\n') character after every 76 characters and always
51    at the end of the encoded string.
52    """
53    return salt.utils.stringutils.to_unicode(
54        base64.encodebytes(salt.utils.stringutils.to_bytes(instr)),
55        encoding="utf8" if salt.utils.platform.is_windows() else None,
56    )
57
58
59def base64_decodestring(instr):
60    """
61    Decode a base64-encoded byte-like object using the "modern" Python interface.
62    """
63    bvalue = salt.utils.stringutils.to_bytes(instr)
64    decoded = base64.decodebytes(bvalue)
65    try:
66        return salt.utils.stringutils.to_unicode(
67            decoded, encoding="utf8" if salt.utils.platform.is_windows() else None
68        )
69    except UnicodeDecodeError:
70        return decoded
71
72
73@jinja_filter("md5")
74def md5_digest(instr):
75    """
76    Generate an md5 hash of a given string.
77    """
78    return salt.utils.stringutils.to_unicode(
79        hashlib.md5(salt.utils.stringutils.to_bytes(instr)).hexdigest()
80    )
81
82
83@jinja_filter("sha1")
84def sha1_digest(instr):
85    """
86    Generate an sha1 hash of a given string.
87    """
88    return hashlib.sha1(salt.utils.stringutils.to_bytes(instr)).hexdigest()
89
90
91@jinja_filter("sha256")
92def sha256_digest(instr):
93    """
94    Generate a sha256 hash of a given string.
95    """
96    return salt.utils.stringutils.to_unicode(
97        hashlib.sha256(salt.utils.stringutils.to_bytes(instr)).hexdigest()
98    )
99
100
101@jinja_filter("sha512")
102def sha512_digest(instr):
103    """
104    Generate a sha512 hash of a given string
105    """
106    return salt.utils.stringutils.to_unicode(
107        hashlib.sha512(salt.utils.stringutils.to_bytes(instr)).hexdigest()
108    )
109
110
111@jinja_filter("hmac")
112def hmac_signature(string, shared_secret, challenge_hmac):
113    """
114    Verify a challenging hmac signature against a string / shared-secret
115    Returns a boolean if the verification succeeded or failed.
116    """
117    msg = salt.utils.stringutils.to_bytes(string)
118    key = salt.utils.stringutils.to_bytes(shared_secret)
119    challenge = salt.utils.stringutils.to_bytes(challenge_hmac)
120    hmac_hash = hmac.new(key, msg, hashlib.sha256)
121    valid_hmac = base64.b64encode(hmac_hash.digest())
122    return valid_hmac == challenge
123
124
125@jinja_filter("hmac_compute")
126def hmac_compute(string, shared_secret):
127    """
128    Create an hmac digest.
129    """
130    msg = salt.utils.stringutils.to_bytes(string)
131    key = salt.utils.stringutils.to_bytes(shared_secret)
132    hmac_hash = hmac.new(key, msg, hashlib.sha256).hexdigest()
133    return hmac_hash
134
135
136@jinja_filter("rand_str")
137@jinja_filter("random_hash")
138def random_hash(size=9999999999, hash_type=None):
139    """
140    Return a hash of a randomized data from random.SystemRandom()
141    """
142    if not hash_type:
143        hash_type = "md5"
144    hasher = getattr(hashlib, hash_type)
145    return hasher(
146        salt.utils.stringutils.to_bytes(str(random.SystemRandom().randint(0, size)))
147    ).hexdigest()
148
149
150@jinja_filter("file_hashsum")
151def get_hash(path, form="sha256", chunk_size=65536):
152    """
153    Get the hash sum of a file
154
155    This is better than ``get_sum`` for the following reasons:
156        - It does not read the entire file into memory.
157        - It does not return a string on error. The returned value of
158            ``get_sum`` cannot really be trusted since it is vulnerable to
159            collisions: ``get_sum(..., 'xyz') == 'Hash xyz not supported'``
160    """
161    hash_type = hasattr(hashlib, form) and getattr(hashlib, form) or None
162    if hash_type is None:
163        raise ValueError("Invalid hash type: {}".format(form))
164
165    with salt.utils.files.fopen(path, "rb") as ifile:
166        hash_obj = hash_type()
167        # read the file in in chunks, not the entire file
168        for chunk in iter(lambda: ifile.read(chunk_size), b""):
169            hash_obj.update(chunk)
170        return hash_obj.hexdigest()
171
172
173class DigestCollector:
174    """
175    Class to collect digest of the file tree.
176    """
177
178    def __init__(self, form="sha256", buff=0x10000):
179        """
180        Constructor of the class.
181        :param form:
182        """
183        self.__digest = hasattr(hashlib, form) and getattr(hashlib, form)() or None
184        if self.__digest is None:
185            raise ValueError("Invalid hash type: {}".format(form))
186        self.__buff = buff
187
188    def add(self, path):
189        """
190        Update digest with the file content by path.
191
192        :param path:
193        :return:
194        """
195        with salt.utils.files.fopen(path, "rb") as ifile:
196            for chunk in iter(lambda: ifile.read(self.__buff), b""):
197                self.__digest.update(chunk)
198
199    def digest(self):
200        """
201        Get digest.
202
203        :return:
204        """
205
206        return salt.utils.stringutils.to_str(self.__digest.hexdigest() + os.linesep)
207