File size: 2,598 Bytes
c87f72b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
"""
Hardware handshake: HMAC-based device authentication.

A device proves its identity by computing an HMAC over a server-issued
challenge using the shared hardware ID as the key. The hardware ID is read
from the ``ARK_S10_HWID`` environment variable.

This implementation uses :func:`hmac.compare_digest` for constant-time
comparison to avoid timing side-channels.
"""

from __future__ import annotations

import hashlib
import hmac
import os
import secrets
from typing import Optional

from .constants import DEFAULT_HMAC_ALGORITHM

HWID_ENV_VAR = "ARK_S10_HWID"


class DeviceAuthError(Exception):
    """Raised when device authentication fails or cannot be performed."""


class HardwareHandshake:
    """Perform HMAC-based device authentication.

    Parameters
    ----------
    hwid:
        Shared hardware identifier used as the HMAC key. If ``None`` (the
        default) the value is read from the ``ARK_S10_HWID`` environment
        variable.
    algorithm:
        Hash algorithm name accepted by :mod:`hashlib`. Defaults to
        ``"sha256"``.
    """

    def __init__(
        self,
        hwid: Optional[str] = None,
        algorithm: str = DEFAULT_HMAC_ALGORITHM,
    ) -> None:
        resolved = hwid if hwid is not None else os.environ.get(HWID_ENV_VAR)
        if not resolved:
            raise DeviceAuthError(
                f"hardware ID not provided and {HWID_ENV_VAR} is not set"
            )
        if algorithm not in hashlib.algorithms_available:
            raise DeviceAuthError(f"unsupported hash algorithm: {algorithm!r}")

        self._key = resolved.encode("utf-8")
        self.algorithm = algorithm

    @staticmethod
    def generate_challenge(nbytes: int = 32) -> bytes:
        """Return a cryptographically random challenge of ``nbytes`` bytes."""
        if nbytes <= 0:
            raise ValueError("nbytes must be positive")
        return secrets.token_bytes(nbytes)

    def sign(self, challenge: bytes) -> str:
        """Compute the hex-encoded HMAC of ``challenge``."""
        if not isinstance(challenge, (bytes, bytearray)):
            raise TypeError("challenge must be bytes")
        return hmac.new(self._key, bytes(challenge), self.algorithm).hexdigest()

    def verify(self, challenge: bytes, signature: str) -> bool:
        """Return ``True`` iff ``signature`` is a valid HMAC for ``challenge``.

        Comparison is performed in constant time.
        """
        if not isinstance(signature, str):
            return False
        expected = self.sign(challenge)
        return hmac.compare_digest(expected, signature)