Adafruit_CircuitPython_hashlib/adafruit_hashlib/_sha1.py
2025-05-16 15:59:09 +00:00

208 lines
5.8 KiB
Python
Executable file

# SPDX-FileCopyrightText: 2013-2015 AJ Alt
# SPDX-FileCopyrightText: 2019 Brent Rubell for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`_sha1.py`
======================================================
SHA1 Hash Algorithm.
Pure-Python implementation by AJ Alt
https://github.com/ajalt/python-sha1/blob/master/sha1.py
Modified by Brent Rubell, 2019
* Author(s): AJ Alt, Brent Rubell
"""
import struct
from io import BytesIO
from micropython import const
try:
from typing import Optional, Tuple, Union
except ImportError:
# suppress because typing does not exist on circuitpython
pass
# SHA Block size and message digest sizes, in bytes.
SHA_BLOCKSIZE = 64
SHA_DIGESTSIZE = 20
# initial hash value [FIPS 5.3.1]
K0 = const(0x5A827999)
K1 = const(0x6ED9EBA1)
K2 = const(0x8F1BBCDC)
K3 = const(0xCA62C1D6)
def _getbuf(data: Union[str, bytes]) -> bytes:
"""Converts data into ascii,
returns bytes of data.
:param str bytes bytearray data: Data to convert.
"""
if isinstance(data, str):
return data.encode("ascii")
return bytes(data)
def _left_rotate(n: int, b: int) -> int:
"""Left rotate a 32-bit integer, n, by b bits.
:param int n: 32-bit integer
:param int b: Desired rotation amount, in bits.
"""
return ((n << b) | (n >> (32 - b))) & 0xFFFFFFFF
def _hash_computation(
chunk: bytes, h0: int, h1: int, h2: int, h3: int, h4: int
) -> Tuple[int, int, int, int, int]:
"""Processes 64-bit chunk of data and returns new digest variables.
Per FIPS [6.1.2]
:param bytes bytearray chunk: 64-bit bytearray
:param list h_tuple: List of hash values for the chunk
"""
assert len(chunk) == 64, "Chunk size should be 64-bits"
w = [0] * 80
# Break chunk into sixteen 4-byte big-endian words w[i]
for i in range(16):
w[i] = struct.unpack(b">I", chunk[i * 4 : i * 4 + 4])[0]
# Extend the sixteen 4-byte words into eighty 4-byte words
for i in range(16, 80):
w[i] = _left_rotate(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1)
# Init. hash values for chunk
a = h0
b = h1
c = h2
d = h3
e = h4
for i in range(80):
if 0 <= i <= 19:
# Use alternative 1 for f from FIPS PB 180-1 to avoid bitwise not
f = d ^ (b & (c ^ d))
k = K0
elif 20 <= i <= 39:
f = b ^ c ^ d
k = K1
elif 40 <= i <= 59:
f = (b & c) | (b & d) | (c & d)
k = K2
elif 60 <= i <= 79:
f = b ^ c ^ d
k = K3
a, b, c, d, e = (
(_left_rotate(a, 5) + f + e + k + w[i]) & 0xFFFFFFFF,
a,
_left_rotate(b, 30),
c,
d,
)
# Add to chunk's hash result so far
h0 = (h0 + a) & 0xFFFFFFFF
h1 = (h1 + b) & 0xFFFFFFFF
h2 = (h2 + c) & 0xFFFFFFFF
h3 = (h3 + d) & 0xFFFFFFFF
h4 = (h4 + e) & 0xFFFFFFFF
return h0, h1, h2, h3, h4
class sha1:
"""SHA-1 Hash Object"""
digest_size = SHA_DIGESTSIZE
block_size = SHA_BLOCKSIZE
name = "sha1"
def __init__(self, data: Optional[Union[str, bytes]] = None):
"""Construct a SHA-1 hash object.
:param bytes data: Optional data to process
"""
# Initial Digest Variables
self._h = (0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0)
# bytes object with 0 <= len < 64 used to store the end of the message
# if the message length is not congruent to 64
self._unprocessed = b""
# Length in bytes of all data that has been processed so far
self._msg_byte_len = 0
if data:
self.update(data)
def _create_digest(self):
"""Returns finalized digest variables for the data processed so far."""
# pre-processing
message = self._unprocessed
message_len = self._msg_byte_len + len(message)
# add trailing '1' bit (+ 0's padding) to string [FIPS 5.1.1]
message += b"\x80"
# append 0 <= k < 512 bits '0', so that the resulting message length (in bytes)
# is congruent to 56 (mod 64)
message += b"\x00" * ((56 - (message_len + 1) % 64) % 64)
# append ml, the original message length, as a 64-bit big-endian integer.
message_bit_length = message_len * 8
message += struct.pack(b">Q", message_bit_length)
# Process the final chunk
h = _hash_computation(message[:64], *self._h)
if len(message) == 64:
return h
return _hash_computation(message[64:], *h)
def update(self, data: Optional[Union[str, bytes]]):
"""Updates the hash object with bytes-like object, data.
:param bytes data: bytearray or bytes object
"""
# if we get a string, convert to a bytearray objects
data = _getbuf(data)
# Use BytesIO for stream-like reading
if isinstance(data, (bytes, bytearray)):
data = BytesIO(data)
# Try to build a chunk out of the unprocessed data, if any
chunk = self._unprocessed + data.read(64 - len(self._unprocessed))
while len(chunk) == 64:
self._h = _hash_computation(chunk, *self._h)
# increase the length of the message by 64 bytes
self._msg_byte_len += 64
# read the next 64 bytes
chunk = data.read(64)
self._unprocessed = chunk
return self
def digest(self):
"""Returns the digest of the data passed to the update()
method so far.
"""
return b"".join(struct.pack(b">I", h) for h in self._create_digest())
def hexdigest(self):
"""Like digest() except the digest is returned as a string object of
double length, containing only hexadecimal digits.
"""
return "".join(["%.2x" % i for i in self.digest()])