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.
- openlinktoken/__init__.py +15 -0
- openlinktoken/attributes/__init__.py +17 -0
- openlinktoken/attributes/attribute.py +26 -0
- openlinktoken/attributes/attribute_expression.py +232 -0
- openlinktoken/attributes/attribute_loader.py +48 -0
- openlinktoken/attributes/base_attribute.py +31 -0
- openlinktoken/attributes/combined_attribute.py +69 -0
- openlinktoken/attributes/general/__init__.py +17 -0
- openlinktoken/attributes/general/date_attribute.py +117 -0
- openlinktoken/attributes/general/decimal_attribute.py +86 -0
- openlinktoken/attributes/general/integer_attribute.py +78 -0
- openlinktoken/attributes/general/record_id_attribute.py +41 -0
- openlinktoken/attributes/general/string_attribute.py +55 -0
- openlinktoken/attributes/general/year_attribute.py +78 -0
- openlinktoken/attributes/person/__init__.py +25 -0
- openlinktoken/attributes/person/age_attribute.py +41 -0
- openlinktoken/attributes/person/birth_date_attribute.py +40 -0
- openlinktoken/attributes/person/birth_year_attribute.py +39 -0
- openlinktoken/attributes/person/canadian_postal_code_attribute.py +165 -0
- openlinktoken/attributes/person/first_name_attribute.py +112 -0
- openlinktoken/attributes/person/last_name_attribute.py +146 -0
- openlinktoken/attributes/person/postal_code_attribute.py +42 -0
- openlinktoken/attributes/person/sex_attribute.py +48 -0
- openlinktoken/attributes/person/social_security_number_attribute.py +119 -0
- openlinktoken/attributes/person/us_postal_code_attribute.py +104 -0
- openlinktoken/attributes/serializable_attribute.py +7 -0
- openlinktoken/attributes/utilities/__init__.py +7 -0
- openlinktoken/attributes/utilities/attribute_utilities.py +224 -0
- openlinktoken/attributes/validation/__init__.py +23 -0
- openlinktoken/attributes/validation/age_range_validator.py +41 -0
- openlinktoken/attributes/validation/attribute_validator.py +22 -0
- openlinktoken/attributes/validation/date_range_validator.py +111 -0
- openlinktoken/attributes/validation/not_in_validator.py +48 -0
- openlinktoken/attributes/validation/not_null_or_empty_validator.py +21 -0
- openlinktoken/attributes/validation/not_starts_with_validator.py +48 -0
- openlinktoken/attributes/validation/regex_validator.py +44 -0
- openlinktoken/attributes/validation/serializable_attribute_validator.py +26 -0
- openlinktoken/attributes/validation/year_range_validator.py +48 -0
- openlinktoken/ec_key_utils.py +207 -0
- openlinktoken/exchange_config.py +305 -0
- openlinktoken/exchange_jwe.py +107 -0
- openlinktoken/metadata.py +104 -0
- openlinktoken/tokens/__init__.py +19 -0
- openlinktoken/tokens/base_token_definition.py +48 -0
- openlinktoken/tokens/definitions/t1_token.py +39 -0
- openlinktoken/tokens/definitions/t2_token.py +39 -0
- openlinktoken/tokens/definitions/t3_token.py +39 -0
- openlinktoken/tokens/definitions/t4_token.py +37 -0
- openlinktoken/tokens/definitions/t5_token.py +37 -0
- openlinktoken/tokens/token.py +38 -0
- openlinktoken/tokens/token_definition.py +46 -0
- openlinktoken/tokens/token_generation_exception.py +16 -0
- openlinktoken/tokens/token_generator.py +201 -0
- openlinktoken/tokens/token_generator_result.py +33 -0
- openlinktoken/tokens/token_registry.py +46 -0
- openlinktoken/tokens/tokenizer/__init__.py +11 -0
- openlinktoken/tokens/tokenizer/passthrough_tokenizer.py +66 -0
- openlinktoken/tokens/tokenizer/sha256_tokenizer.py +72 -0
- openlinktoken/tokens/tokenizer/tokenizer.py +31 -0
- openlinktoken/tokentransformer/__init__.py +43 -0
- openlinktoken/tokentransformer/decrypt_token_transformer.py +83 -0
- openlinktoken/tokentransformer/encrypt_token_transformer.py +85 -0
- openlinktoken/tokentransformer/encryption_constants.py +23 -0
- openlinktoken/tokentransformer/hash_token_transformer.py +88 -0
- openlinktoken/tokentransformer/jwe_match_token_formatter.py +128 -0
- openlinktoken/tokentransformer/match_token_constants.py +57 -0
- openlinktoken/tokentransformer/no_operation_token_transformer.py +24 -0
- openlinktoken/tokentransformer/token_transformer.py +22 -0
- openlinktoken-2.0.0.dist-info/METADATA +21 -0
- openlinktoken-2.0.0.dist-info/RECORD +72 -0
- openlinktoken-2.0.0.dist-info/WHEEL +5 -0
- 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()
|