openlinktoken 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. openlinktoken/__init__.py +15 -0
  2. openlinktoken/attributes/__init__.py +17 -0
  3. openlinktoken/attributes/attribute.py +26 -0
  4. openlinktoken/attributes/attribute_expression.py +232 -0
  5. openlinktoken/attributes/attribute_loader.py +48 -0
  6. openlinktoken/attributes/base_attribute.py +31 -0
  7. openlinktoken/attributes/combined_attribute.py +69 -0
  8. openlinktoken/attributes/general/__init__.py +17 -0
  9. openlinktoken/attributes/general/date_attribute.py +117 -0
  10. openlinktoken/attributes/general/decimal_attribute.py +86 -0
  11. openlinktoken/attributes/general/integer_attribute.py +78 -0
  12. openlinktoken/attributes/general/record_id_attribute.py +41 -0
  13. openlinktoken/attributes/general/string_attribute.py +55 -0
  14. openlinktoken/attributes/general/year_attribute.py +78 -0
  15. openlinktoken/attributes/person/__init__.py +25 -0
  16. openlinktoken/attributes/person/age_attribute.py +41 -0
  17. openlinktoken/attributes/person/birth_date_attribute.py +40 -0
  18. openlinktoken/attributes/person/birth_year_attribute.py +39 -0
  19. openlinktoken/attributes/person/canadian_postal_code_attribute.py +165 -0
  20. openlinktoken/attributes/person/first_name_attribute.py +112 -0
  21. openlinktoken/attributes/person/last_name_attribute.py +146 -0
  22. openlinktoken/attributes/person/postal_code_attribute.py +42 -0
  23. openlinktoken/attributes/person/sex_attribute.py +48 -0
  24. openlinktoken/attributes/person/social_security_number_attribute.py +119 -0
  25. openlinktoken/attributes/person/us_postal_code_attribute.py +104 -0
  26. openlinktoken/attributes/serializable_attribute.py +7 -0
  27. openlinktoken/attributes/utilities/__init__.py +7 -0
  28. openlinktoken/attributes/utilities/attribute_utilities.py +224 -0
  29. openlinktoken/attributes/validation/__init__.py +23 -0
  30. openlinktoken/attributes/validation/age_range_validator.py +41 -0
  31. openlinktoken/attributes/validation/attribute_validator.py +22 -0
  32. openlinktoken/attributes/validation/date_range_validator.py +111 -0
  33. openlinktoken/attributes/validation/not_in_validator.py +48 -0
  34. openlinktoken/attributes/validation/not_null_or_empty_validator.py +21 -0
  35. openlinktoken/attributes/validation/not_starts_with_validator.py +48 -0
  36. openlinktoken/attributes/validation/regex_validator.py +44 -0
  37. openlinktoken/attributes/validation/serializable_attribute_validator.py +26 -0
  38. openlinktoken/attributes/validation/year_range_validator.py +48 -0
  39. openlinktoken/ec_key_utils.py +207 -0
  40. openlinktoken/exchange_config.py +305 -0
  41. openlinktoken/exchange_jwe.py +107 -0
  42. openlinktoken/metadata.py +104 -0
  43. openlinktoken/tokens/__init__.py +19 -0
  44. openlinktoken/tokens/base_token_definition.py +48 -0
  45. openlinktoken/tokens/definitions/t1_token.py +39 -0
  46. openlinktoken/tokens/definitions/t2_token.py +39 -0
  47. openlinktoken/tokens/definitions/t3_token.py +39 -0
  48. openlinktoken/tokens/definitions/t4_token.py +37 -0
  49. openlinktoken/tokens/definitions/t5_token.py +37 -0
  50. openlinktoken/tokens/token.py +38 -0
  51. openlinktoken/tokens/token_definition.py +46 -0
  52. openlinktoken/tokens/token_generation_exception.py +16 -0
  53. openlinktoken/tokens/token_generator.py +201 -0
  54. openlinktoken/tokens/token_generator_result.py +33 -0
  55. openlinktoken/tokens/token_registry.py +46 -0
  56. openlinktoken/tokens/tokenizer/__init__.py +11 -0
  57. openlinktoken/tokens/tokenizer/passthrough_tokenizer.py +66 -0
  58. openlinktoken/tokens/tokenizer/sha256_tokenizer.py +72 -0
  59. openlinktoken/tokens/tokenizer/tokenizer.py +31 -0
  60. openlinktoken/tokentransformer/__init__.py +43 -0
  61. openlinktoken/tokentransformer/decrypt_token_transformer.py +83 -0
  62. openlinktoken/tokentransformer/encrypt_token_transformer.py +85 -0
  63. openlinktoken/tokentransformer/encryption_constants.py +23 -0
  64. openlinktoken/tokentransformer/hash_token_transformer.py +88 -0
  65. openlinktoken/tokentransformer/jwe_match_token_formatter.py +128 -0
  66. openlinktoken/tokentransformer/match_token_constants.py +57 -0
  67. openlinktoken/tokentransformer/no_operation_token_transformer.py +24 -0
  68. openlinktoken/tokentransformer/token_transformer.py +22 -0
  69. openlinktoken-2.0.0.dist-info/METADATA +21 -0
  70. openlinktoken-2.0.0.dist-info/RECORD +72 -0
  71. openlinktoken-2.0.0.dist-info/WHEEL +5 -0
  72. openlinktoken-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,33 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from typing import Dict, Set
4
+
5
+
6
+ class TokenGeneratorResult:
7
+ """
8
+ Represents the result of a token generation operation.
9
+
10
+ This class contains the generated tokens, the list of invalid attributes,
11
+ and the count of blank tokens generated by rule.
12
+ """
13
+
14
+ def __init__(self):
15
+ """Initialize the result with empty collections."""
16
+ self._tokens: Dict[str, str] = {}
17
+ self._invalid_attributes: Set[str] = set()
18
+ self._blank_tokens_by_rule: Set[str] = set()
19
+
20
+ @property
21
+ def tokens(self) -> Dict[str, str]:
22
+ """Get the generated tokens."""
23
+ return self._tokens
24
+
25
+ @property
26
+ def invalid_attributes(self) -> Set[str]:
27
+ """Get the invalid attributes."""
28
+ return self._invalid_attributes
29
+
30
+ @property
31
+ def blank_tokens_by_rule(self) -> Set[str]:
32
+ """Get the blank tokens by rule."""
33
+ return self._blank_tokens_by_rule
@@ -0,0 +1,46 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ import importlib
4
+ import pkgutil
5
+ from importlib import resources
6
+ from typing import Dict, List
7
+
8
+ from openlinktoken.attributes.attribute_expression import AttributeExpression
9
+ from openlinktoken.tokens.token import Token
10
+
11
+
12
+ class TokenRegistry:
13
+ @staticmethod
14
+ def load_all_tokens() -> Dict[str, List[AttributeExpression]]:
15
+ definitions: Dict[str, List[AttributeExpression]] = {}
16
+
17
+ package_name = "openlinktoken.tokens.definitions"
18
+ package = importlib.import_module(package_name)
19
+
20
+ module_names: List[str] = []
21
+ package_path = getattr(package, "__path__", None)
22
+ if package_path is not None:
23
+ try:
24
+ module_names = [module_info.name for module_info in pkgutil.iter_modules(package_path)]
25
+ except Exception:
26
+ module_names = []
27
+
28
+ if not module_names:
29
+ try:
30
+ module_names = [
31
+ item.name[:-3]
32
+ for item in resources.files(package).iterdir()
33
+ if item.name.endswith(".py") and item.name != "__init__.py"
34
+ ]
35
+ except Exception:
36
+ module_names = ["t1_token", "t2_token", "t3_token", "t4_token", "t5_token"]
37
+
38
+ for module_name in module_names:
39
+ module = importlib.import_module(f"{package_name}.{module_name}")
40
+
41
+ for obj in module.__dict__.values():
42
+ if isinstance(obj, type) and issubclass(obj, Token) and obj is not Token:
43
+ token = obj()
44
+ definitions[token.get_identifier()] = token.get_definition()
45
+
46
+ return definitions
@@ -0,0 +1,11 @@
1
+ """Tokenizer implementations for token generation."""
2
+
3
+ from .passthrough_tokenizer import PassthroughTokenizer
4
+ from .sha256_tokenizer import SHA256Tokenizer
5
+ from .tokenizer import Tokenizer
6
+
7
+ __all__ = [
8
+ "PassthroughTokenizer",
9
+ "SHA256Tokenizer",
10
+ "Tokenizer",
11
+ ]
@@ -0,0 +1,66 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from typing import List
4
+
5
+ from openlinktoken.tokens.token import Token
6
+ from openlinktoken.tokens.tokenizer.tokenizer import Tokenizer
7
+ from openlinktoken.tokentransformer.token_transformer import TokenTransformer
8
+
9
+
10
+ class PassthroughTokenizer(Tokenizer):
11
+ """
12
+ A tokenizer that returns the input value unchanged (passthrough).
13
+
14
+ This tokenizer does not apply any hashing or cryptographic transformation
15
+ to the input value. It simply returns the input as-is, optionally applying
16
+ token transformers if provided.
17
+
18
+ This is useful for scenarios where you want to preserve the original token
19
+ signature without any cryptographic processing, while still allowing for
20
+ optional transformations (e.g., encryption).
21
+ """
22
+
23
+ EMPTY = Token.BLANK
24
+ """
25
+ The empty token value.
26
+
27
+ This is the value returned when the token signature is None or blank.
28
+ """
29
+
30
+ def __init__(self, token_transformer_list: List[TokenTransformer]):
31
+ """
32
+ Initialize the tokenizer.
33
+
34
+ Args:
35
+ token_transformer_list: A list of token transformers.
36
+ """
37
+ self.token_transformer_list = token_transformer_list
38
+
39
+ def tokenize(self, value: str) -> str:
40
+ """
41
+ Return the input value unchanged (passthrough).
42
+
43
+ Token = value
44
+
45
+ The token is optionally transformed with one or more transformers.
46
+
47
+ Args:
48
+ value: The token signature value.
49
+
50
+ Returns:
51
+ The token. If the token signature value is None or blank,
52
+ EMPTY is returned.
53
+
54
+ Raises:
55
+ Exception: If an error is thrown by the transformer.
56
+ """
57
+ if value is None or value.strip() == "":
58
+ return self.EMPTY
59
+
60
+ transformed_token = value
61
+
62
+ # Apply transformers
63
+ for token_transformer in self.token_transformer_list:
64
+ transformed_token = token_transformer.transform(transformed_token)
65
+
66
+ return transformed_token
@@ -0,0 +1,72 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ import hashlib
4
+ from typing import List
5
+
6
+ from openlinktoken.tokens.token import Token
7
+ from openlinktoken.tokens.tokenizer.tokenizer import Tokenizer
8
+ from openlinktoken.tokentransformer.token_transformer import TokenTransformer
9
+
10
+
11
+ class SHA256Tokenizer(Tokenizer):
12
+ """
13
+ Generates token using SHA256 digest.
14
+
15
+ The token is generated using SHA256 digest and is hex encoded.
16
+ If token transformations are specified, the token is then transformed
17
+ by those transformers.
18
+ """
19
+
20
+ EMPTY = Token.BLANK
21
+ """
22
+ The empty token value.
23
+
24
+ This is the value returned when the token signature is None or blank.
25
+ """
26
+
27
+ def __init__(self, token_transformer_list: List[TokenTransformer]):
28
+ """
29
+ Initialize the tokenizer.
30
+
31
+ Args:
32
+ token_transformer_list: A list of token transformers.
33
+ """
34
+ self.token_transformer_list = token_transformer_list
35
+
36
+ def tokenize(self, value: str) -> str:
37
+ """
38
+ Generate the token for the given token signature.
39
+
40
+ Token = Hex(Sha256(token-signature))
41
+
42
+ The token is optionally transformed with one or more transformers.
43
+
44
+ Args:
45
+ value: The token signature value.
46
+
47
+ Returns:
48
+ The token. If the token signature value is None or blank,
49
+ EMPTY is returned.
50
+
51
+ Raises:
52
+ Exception: If an error is thrown by the transformer.
53
+ """
54
+ if value is None or value.strip() == "":
55
+ return self.EMPTY
56
+
57
+ # Convert string to bytes using UTF-8 encoding
58
+ value_bytes = value.encode("utf-8")
59
+
60
+ # Create SHA-256 hash
61
+ digest = hashlib.sha256()
62
+ digest.update(value_bytes)
63
+ hash_bytes = digest.digest()
64
+
65
+ # Convert to hex string
66
+ transformed_token = hash_bytes.hex()
67
+
68
+ # Apply transformers
69
+ for token_transformer in self.token_transformer_list:
70
+ transformed_token = token_transformer.transform(transformed_token)
71
+
72
+ return transformed_token
@@ -0,0 +1,31 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class Tokenizer(ABC):
7
+ """
8
+ Interface for tokenizing values into tokens.
9
+
10
+ A tokenizer takes a string value (typically a token signature) and
11
+ transforms it into a token using various cryptographic or encoding
12
+ techniques.
13
+ """
14
+
15
+ @abstractmethod
16
+ def tokenize(self, value: str) -> str:
17
+ """
18
+ Generate a token from the given value.
19
+
20
+ Args:
21
+ value: The value to tokenize (e.g., a token signature).
22
+
23
+ Returns:
24
+ The generated token. Returns a default empty token value
25
+ if the input value is null or blank.
26
+
27
+ Raises:
28
+ Exception: If an error occurs during tokenization, such as
29
+ unsupported encoding or cryptographic algorithm issues.
30
+ """
31
+ pass
@@ -0,0 +1,43 @@
1
+ """Token transformer implementations for Open Link Token."""
2
+
3
+ from .decrypt_token_transformer import DecryptTokenTransformer
4
+ from .encrypt_token_transformer import EncryptTokenTransformer
5
+ from .hash_token_transformer import HashTokenTransformer
6
+ from .match_token_constants import (
7
+ HEADER_KEY_ALGORITHM,
8
+ HEADER_KEY_ENCRYPTION,
9
+ HEADER_KEY_KEY_ID,
10
+ HEADER_KEY_TYPE,
11
+ PAYLOAD_KEY_HASH_ALGORITHM,
12
+ PAYLOAD_KEY_ISSUED_AT,
13
+ PAYLOAD_KEY_ISSUER,
14
+ PAYLOAD_KEY_MAC_ALGORITHM,
15
+ PAYLOAD_KEY_PPID,
16
+ PAYLOAD_KEY_RING_ID,
17
+ PAYLOAD_KEY_RULE_ID,
18
+ TOKEN_TYPE,
19
+ V1_TOKEN_PREFIX,
20
+ )
21
+ from .no_operation_token_transformer import NoOperationTokenTransformer
22
+ from .token_transformer import TokenTransformer
23
+
24
+ __all__ = [
25
+ "DecryptTokenTransformer",
26
+ "EncryptTokenTransformer",
27
+ "HashTokenTransformer",
28
+ "HEADER_KEY_ALGORITHM",
29
+ "HEADER_KEY_ENCRYPTION",
30
+ "HEADER_KEY_KEY_ID",
31
+ "HEADER_KEY_TYPE",
32
+ "PAYLOAD_KEY_HASH_ALGORITHM",
33
+ "PAYLOAD_KEY_ISSUED_AT",
34
+ "PAYLOAD_KEY_ISSUER",
35
+ "PAYLOAD_KEY_MAC_ALGORITHM",
36
+ "PAYLOAD_KEY_PPID",
37
+ "PAYLOAD_KEY_RING_ID",
38
+ "PAYLOAD_KEY_RULE_ID",
39
+ "TOKEN_TYPE",
40
+ "V1_TOKEN_PREFIX",
41
+ "NoOperationTokenTransformer",
42
+ "TokenTransformer",
43
+ ]
@@ -0,0 +1,83 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ import base64
4
+ import logging
5
+ from typing import Union
6
+
7
+ from cryptography.hazmat.backends import default_backend
8
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
9
+
10
+ from openlinktoken.tokentransformer.encryption_constants import EncryptionConstants
11
+ from openlinktoken.tokentransformer.token_transformer import TokenTransformer
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class DecryptTokenTransformer(TokenTransformer):
17
+ """AES-256 GCM token decryption transformer.
18
+
19
+ Parity with Java `DecryptTokenTransformer`:
20
+ - Java prepends IV to (ciphertext || tag) and GCM consumes combined remainder.
21
+ - Python splits IV, ciphertext, and tag explicitly before constructing GCM mode.
22
+ - Both expect 12-byte IV and 16-byte tag; key length enforced at init.
23
+ """
24
+
25
+ def __init__(self, encryption_key: Union[str, bytes]):
26
+ """
27
+ Initializes the underlying cipher (AES) with the decryption secret.
28
+
29
+ Accepts either a ``str`` (UTF-8 encoded; must encode to exactly 32 bytes) or
30
+ raw ``bytes`` (must be exactly 32 bytes).
31
+
32
+ Args:
33
+ encryption_key: The encryption key. The UTF-8 encoded key material must be exactly 32 bytes long.
34
+
35
+ Raises:
36
+ ValueError: If the encryption key material is not exactly 32 bytes long.
37
+ """
38
+ if isinstance(encryption_key, bytes):
39
+ if len(encryption_key) != EncryptionConstants.KEY_BYTE_LENGTH:
40
+ logger.error(f"Invalid Argument. Key must be {EncryptionConstants.KEY_BYTE_LENGTH} bytes long")
41
+ raise ValueError(f"Key must be {EncryptionConstants.KEY_BYTE_LENGTH} bytes long")
42
+ self.encryption_key = encryption_key
43
+ else:
44
+ encryption_key_bytes = encryption_key.encode("utf-8")
45
+ if len(encryption_key_bytes) != EncryptionConstants.KEY_BYTE_LENGTH:
46
+ logger.error(f"Invalid Argument. Key must be {EncryptionConstants.KEY_BYTE_LENGTH} bytes long")
47
+ raise ValueError(f"Key must be {EncryptionConstants.KEY_BYTE_LENGTH} bytes long")
48
+ self.encryption_key = encryption_key_bytes
49
+
50
+ def transform(self, token: str) -> str:
51
+ """Decrypt a base64 encoded token produced by the encrypt transformer.
52
+
53
+ Args:
54
+ token: Base64 string containing IV || ciphertext || tag.
55
+
56
+ Returns:
57
+ Decrypted UTF-8 token string.
58
+
59
+ Raises:
60
+ Exception: Propagates cryptographic failures (logged here first).
61
+ """
62
+ try:
63
+ # Decode the base64-encoded token
64
+ message_bytes = base64.b64decode(token)
65
+
66
+ # Extract IV, encrypted data, and tag
67
+ iv_bytes = message_bytes[: EncryptionConstants.IV_SIZE]
68
+ ciphertext_and_tag = message_bytes[EncryptionConstants.IV_SIZE :]
69
+ ciphertext = ciphertext_and_tag[: -EncryptionConstants.TAG_LENGTH_BYTES]
70
+ tag = ciphertext_and_tag[-EncryptionConstants.TAG_LENGTH_BYTES :]
71
+
72
+ # Create cipher for decryption
73
+ cipher = Cipher(algorithms.AES(self.encryption_key), modes.GCM(iv_bytes, tag), backend=default_backend())
74
+
75
+ # Decrypt the token
76
+ decryptor = cipher.decryptor()
77
+ decrypted_bytes = decryptor.update(ciphertext) + decryptor.finalize()
78
+
79
+ return decrypted_bytes.decode("utf-8")
80
+
81
+ except Exception as e:
82
+ logger.error("Error during token decryption", exc_info=e)
83
+ raise
@@ -0,0 +1,85 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ import base64
4
+ import logging
5
+ import secrets
6
+ from typing import Union
7
+
8
+ from cryptography.hazmat.backends import default_backend
9
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
10
+
11
+ from openlinktoken.tokentransformer.encryption_constants import EncryptionConstants
12
+ from openlinktoken.tokentransformer.token_transformer import TokenTransformer
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class EncryptTokenTransformer(TokenTransformer):
18
+ """
19
+ Transforms the token using AES-256 symmetric encryption.
20
+
21
+ See: https://datatracker.ietf.org/doc/html/rfc3826 (AES)
22
+ """
23
+
24
+ def __init__(self, encryption_key: Union[str, bytes]):
25
+ """
26
+ Initializes the underlying cipher (AES) with the encryption secret.
27
+
28
+ Accepts either a ``str`` (UTF-8 encoded; must encode to exactly 32 bytes) or
29
+ raw ``bytes`` (must be exactly 32 bytes).
30
+
31
+ Args:
32
+ encryption_key: The encryption key. The UTF-8 encoded key material must be exactly 32 bytes long.
33
+
34
+ Raises:
35
+ ValueError: If the encryption key material is not exactly 32 bytes long.
36
+ """
37
+ if isinstance(encryption_key, bytes):
38
+ if len(encryption_key) != EncryptionConstants.KEY_BYTE_LENGTH:
39
+ logger.error(f"Invalid Argument. Key must be {EncryptionConstants.KEY_BYTE_LENGTH} bytes long")
40
+ raise ValueError(f"Key must be {EncryptionConstants.KEY_BYTE_LENGTH} bytes long")
41
+ self.encryption_key = encryption_key
42
+ else:
43
+ encryption_key_bytes = encryption_key.encode("utf-8")
44
+ if len(encryption_key_bytes) != EncryptionConstants.KEY_BYTE_LENGTH:
45
+ logger.error(f"Invalid Argument. Key must be {EncryptionConstants.KEY_BYTE_LENGTH} bytes long")
46
+ raise ValueError(f"Key must be {EncryptionConstants.KEY_BYTE_LENGTH} bytes long")
47
+ self.encryption_key = encryption_key_bytes
48
+
49
+ def transform(self, token: str) -> str:
50
+ """
51
+ Encryption token transformer.
52
+
53
+ Encrypts the token using AES-256 symmetric encryption algorithm.
54
+
55
+ Args:
56
+ token: The token to be encrypted.
57
+
58
+ Returns:
59
+ The encrypted token in base64 format.
60
+
61
+ Raises:
62
+ Exception: If encryption fails due to various cryptographic errors.
63
+ """
64
+ try:
65
+ # Generate random IV (for AES GCM mode)
66
+ iv_bytes = secrets.token_bytes(EncryptionConstants.IV_SIZE)
67
+
68
+ # Create cipher
69
+ cipher = Cipher(algorithms.AES(self.encryption_key), modes.GCM(iv_bytes), backend=default_backend())
70
+
71
+ # Encrypt the token
72
+ encryptor = cipher.encryptor()
73
+ encrypted_bytes = encryptor.update(token.encode("utf-8")) + encryptor.finalize()
74
+
75
+ # Get the authentication tag
76
+ tag = encryptor.tag
77
+
78
+ # Combine IV + encrypted data + tag
79
+ message_bytes = iv_bytes + encrypted_bytes + tag
80
+
81
+ return base64.b64encode(message_bytes).decode("utf-8")
82
+
83
+ except Exception as e:
84
+ logger.error(f"Error during token encryption: {e}")
85
+ raise
@@ -0,0 +1,23 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+
4
+ class EncryptionConstants:
5
+ """Shared constants for AES-256 GCM encryption and decryption."""
6
+
7
+ # AES algorithm name
8
+ AES = "AES"
9
+
10
+ # AES-256 GCM encryption algorithm specification
11
+ ENCRYPTION_ALGORITHM = "AES/GCM/NoPadding"
12
+
13
+ # AES-256 key length in bytes
14
+ KEY_BYTE_LENGTH = 32
15
+
16
+ # GCM initialization vector size in bytes
17
+ IV_SIZE = 12
18
+
19
+ # GCM authentication tag length in bits
20
+ TAG_LENGTH_BITS = 128
21
+
22
+ # GCM authentication tag length in bytes (128 bits = 16 bytes)
23
+ TAG_LENGTH_BYTES = 16
@@ -0,0 +1,88 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ import base64
4
+ import hashlib
5
+ import hmac
6
+ import logging
7
+ import threading
8
+ from typing import Union
9
+
10
+ from openlinktoken.tokentransformer.token_transformer import TokenTransformer
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class HashTokenTransformer(TokenTransformer):
16
+ """
17
+ Transforms the token using a cryptographic hash function with
18
+ a secret key.
19
+
20
+ See: https://datatracker.ietf.org/doc/html/rfc4868 (HMACSHA256)
21
+ """
22
+
23
+ def __init__(self, hashing_secret: Union[str, bytes, None]):
24
+ """
25
+ Initializes the underlying MAC with the secret key.
26
+
27
+ Accepts a ``str`` (encoded to UTF-8), raw ``bytes``, or ``None`` / empty
28
+ to create a no-op transformer (``transform`` will raise ``RuntimeError``).
29
+
30
+ Args:
31
+ hashing_secret: The cryptographic secret key.
32
+
33
+ Raises:
34
+ ValueError: If the hashing secret is None or empty.
35
+ """
36
+ self._lock = threading.Lock()
37
+ if isinstance(hashing_secret, bytes):
38
+ self.hashing_secret = hashing_secret
39
+ self._mac_available = len(hashing_secret) > 0
40
+ elif not hashing_secret or hashing_secret.strip() == "":
41
+ self.hashing_secret = b""
42
+ self._mac_available = False
43
+ else:
44
+ self.hashing_secret = hashing_secret.encode("utf-8")
45
+ self._mac_available = True
46
+
47
+ def transform(self, token: str) -> str:
48
+ """
49
+ Hash token transformer.
50
+
51
+ The token is transformed using HMAC SHA256 algorithm.
52
+
53
+ Args:
54
+ token: The token to be transformed.
55
+
56
+ Returns:
57
+ Hashed token in base64 format.
58
+
59
+ Raises:
60
+ ValueError: If token is None or blank.
61
+ RuntimeError: If the HMAC is not initialized properly.
62
+ """
63
+ if token is None or token.strip() == "":
64
+ logger.error("Invalid Argument. Token can't be None or blank.")
65
+ raise ValueError("Invalid Argument. Token can't be None or blank.")
66
+
67
+ if not self._mac_available:
68
+ raise RuntimeError("HMAC is not properly initialized due to empty hashing secret.")
69
+
70
+ with self._lock:
71
+ mac = hmac.new(self.hashing_secret, token.encode("utf-8"), hashlib.sha256)
72
+
73
+ # Get the digest and encode to base64
74
+ digest = mac.digest()
75
+ return base64.b64encode(digest).decode("utf-8")
76
+
77
+ def __getstate__(self):
78
+ """Custom serialization support."""
79
+ state = self.__dict__.copy()
80
+ # Remove the lock as it can't be pickled
81
+ del state["_lock"]
82
+ return state
83
+
84
+ def __setstate__(self, state):
85
+ """Custom deserialization support."""
86
+ self.__dict__.update(state)
87
+ # Recreate the lock
88
+ self._lock = threading.Lock()