otdf-python 0.4.0__py3-none-any.whl → 0.4.1__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.
- otdf_python/__init__.py +1 -2
- otdf_python/__main__.py +1 -2
- otdf_python/address_normalizer.py +8 -10
- otdf_python/aesgcm.py +8 -0
- otdf_python/assertion_config.py +21 -0
- otdf_python/asym_crypto.py +18 -22
- otdf_python/auth_headers.py +7 -6
- otdf_python/autoconfigure_utils.py +22 -6
- otdf_python/cli.py +5 -5
- otdf_python/collection_store.py +13 -0
- otdf_python/collection_store_impl.py +5 -0
- otdf_python/config.py +13 -0
- otdf_python/connect_client.py +1 -0
- otdf_python/constants.py +2 -0
- otdf_python/crypto_utils.py +4 -0
- otdf_python/dpop.py +3 -5
- otdf_python/ecc_constants.py +12 -14
- otdf_python/ecc_mode.py +7 -2
- otdf_python/ecdh.py +24 -25
- otdf_python/eckeypair.py +5 -0
- otdf_python/header.py +5 -0
- otdf_python/invalid_zip_exception.py +6 -2
- otdf_python/kas_client.py +48 -55
- otdf_python/kas_connect_rpc_client.py +16 -19
- otdf_python/kas_info.py +4 -3
- otdf_python/kas_key_cache.py +10 -9
- otdf_python/key_type.py +4 -0
- otdf_python/key_type_constants.py +4 -11
- otdf_python/manifest.py +24 -0
- otdf_python/nanotdf.py +34 -24
- otdf_python/nanotdf_ecdsa_struct.py +5 -9
- otdf_python/nanotdf_type.py +12 -0
- otdf_python/policy_binding_serializer.py +6 -4
- otdf_python/policy_info.py +6 -0
- otdf_python/policy_object.py +8 -0
- otdf_python/policy_stub.py +2 -0
- otdf_python/resource_locator.py +22 -13
- otdf_python/sdk.py +49 -57
- otdf_python/sdk_builder.py +58 -41
- otdf_python/sdk_exceptions.py +11 -1
- otdf_python/symmetric_and_payload_config.py +6 -0
- otdf_python/tdf.py +47 -10
- otdf_python/tdf_reader.py +10 -13
- otdf_python/tdf_writer.py +5 -0
- otdf_python/token_source.py +4 -3
- otdf_python/version.py +5 -0
- otdf_python/zip_reader.py +10 -2
- otdf_python/zip_writer.py +11 -0
- {otdf_python-0.4.0.dist-info → otdf_python-0.4.1.dist-info}/METADATA +1 -1
- {otdf_python-0.4.0.dist-info → otdf_python-0.4.1.dist-info}/RECORD +52 -52
- {otdf_python-0.4.0.dist-info → otdf_python-0.4.1.dist-info}/WHEEL +1 -1
- {otdf_python-0.4.0.dist-info → otdf_python-0.4.1.dist-info}/licenses/LICENSE +0 -0
otdf_python/__init__.py
CHANGED
otdf_python/__main__.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Main entry point for running otdf_python as a module.
|
|
2
|
+
"""Main entry point for running otdf_python as a module.
|
|
4
3
|
|
|
5
4
|
This allows the package to be run with `python -m otdf_python` and properly
|
|
6
5
|
handles the CLI interface without import conflicts.
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Address normalization utilities for OpenTDF.
|
|
3
|
-
"""
|
|
1
|
+
"""Address normalization utilities for OpenTDF."""
|
|
4
2
|
|
|
5
3
|
import logging
|
|
6
4
|
import re
|
|
@@ -12,8 +10,7 @@ logger = logging.getLogger(__name__)
|
|
|
12
10
|
|
|
13
11
|
|
|
14
12
|
def normalize_address(url_string: str, use_plaintext: bool) -> str:
|
|
15
|
-
"""
|
|
16
|
-
Normalize a URL address to ensure it has the correct scheme and port.
|
|
13
|
+
"""Normalize a URL address to ensure it has the correct scheme and port.
|
|
17
14
|
|
|
18
15
|
Args:
|
|
19
16
|
url_string: The URL string to normalize
|
|
@@ -24,6 +21,7 @@ def normalize_address(url_string: str, use_plaintext: bool) -> str:
|
|
|
24
21
|
|
|
25
22
|
Raises:
|
|
26
23
|
SDKException: If there's an error parsing or creating the URL
|
|
24
|
+
|
|
27
25
|
"""
|
|
28
26
|
scheme = "http" if use_plaintext else "https"
|
|
29
27
|
|
|
@@ -34,8 +32,8 @@ def normalize_address(url_string: str, use_plaintext: bool) -> str:
|
|
|
34
32
|
port_str = host_port_pattern.group(2)
|
|
35
33
|
try:
|
|
36
34
|
port = int(port_str)
|
|
37
|
-
except ValueError:
|
|
38
|
-
raise SDKException(f"Invalid port in URL [{url_string}]")
|
|
35
|
+
except ValueError as err:
|
|
36
|
+
raise SDKException(f"Invalid port in URL [{url_string}]") from err
|
|
39
37
|
|
|
40
38
|
normalized_url = f"{scheme}://{host}:{port}"
|
|
41
39
|
logger.debug(f"normalized url [{url_string}] to [{normalized_url}]")
|
|
@@ -66,8 +64,8 @@ def normalize_address(url_string: str, use_plaintext: bool) -> str:
|
|
|
66
64
|
_, port_str = parsed_url.netloc.split(":", 1)
|
|
67
65
|
try:
|
|
68
66
|
port = int(port_str)
|
|
69
|
-
except ValueError:
|
|
70
|
-
raise SDKException(f"Invalid port in URL [{url_string}]")
|
|
67
|
+
except ValueError as err:
|
|
68
|
+
raise SDKException(f"Invalid port in URL [{url_string}]") from err
|
|
71
69
|
|
|
72
70
|
# If no port was found or extracted, use the default
|
|
73
71
|
if port is None:
|
|
@@ -81,4 +79,4 @@ def normalize_address(url_string: str, use_plaintext: bool) -> str:
|
|
|
81
79
|
except Exception as e:
|
|
82
80
|
if isinstance(e, SDKException):
|
|
83
81
|
raise e
|
|
84
|
-
raise SDKException(f"Error normalizing URL [{url_string}]"
|
|
82
|
+
raise SDKException(f"Error normalizing URL [{url_string}]: {e}") from e
|
otdf_python/aesgcm.py
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
"""AES-GCM encryption and decryption functionality."""
|
|
2
|
+
|
|
1
3
|
import os
|
|
2
4
|
|
|
3
5
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class AesGcm:
|
|
9
|
+
"""AES-GCM encryption and decryption operations."""
|
|
10
|
+
|
|
7
11
|
GCM_NONCE_LENGTH = 12
|
|
8
12
|
GCM_TAG_LENGTH = 16
|
|
9
13
|
|
|
10
14
|
def __init__(self, key: bytes):
|
|
15
|
+
"""Initialize AES-GCM cipher with key."""
|
|
11
16
|
if not key or len(key) not in (16, 24, 32):
|
|
12
17
|
raise ValueError("Invalid key size for GCM encryption")
|
|
13
18
|
self.key = key
|
|
@@ -17,7 +22,10 @@ class AesGcm:
|
|
|
17
22
|
return self.key
|
|
18
23
|
|
|
19
24
|
class Encrypted:
|
|
25
|
+
"""Encrypted data with initialization vector and ciphertext."""
|
|
26
|
+
|
|
20
27
|
def __init__(self, iv: bytes, ciphertext: bytes):
|
|
28
|
+
"""Initialize encrypted data."""
|
|
21
29
|
self.iv = iv
|
|
22
30
|
self.ciphertext = ciphertext
|
|
23
31
|
|
otdf_python/assertion_config.py
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
"""Assertion configuration for TDF."""
|
|
2
|
+
|
|
1
3
|
from enum import Enum, auto
|
|
2
4
|
from typing import Any
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
class Type(Enum):
|
|
8
|
+
"""Assertion type enumeration."""
|
|
9
|
+
|
|
6
10
|
HANDLING_ASSERTION = "handling"
|
|
7
11
|
BASE_ASSERTION = "base"
|
|
8
12
|
|
|
@@ -11,6 +15,8 @@ class Type(Enum):
|
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
class Scope(Enum):
|
|
18
|
+
"""Assertion scope enumeration."""
|
|
19
|
+
|
|
14
20
|
TRUSTED_DATA_OBJ = "tdo"
|
|
15
21
|
PAYLOAD = "payload"
|
|
16
22
|
|
|
@@ -19,12 +25,16 @@ class Scope(Enum):
|
|
|
19
25
|
|
|
20
26
|
|
|
21
27
|
class AssertionKeyAlg(Enum):
|
|
28
|
+
"""Assertion key algorithm enumeration."""
|
|
29
|
+
|
|
22
30
|
RS256 = auto()
|
|
23
31
|
HS256 = auto()
|
|
24
32
|
NOT_DEFINED = auto()
|
|
25
33
|
|
|
26
34
|
|
|
27
35
|
class AppliesToState(Enum):
|
|
36
|
+
"""Assertion applies-to state enumeration."""
|
|
37
|
+
|
|
28
38
|
ENCRYPTED = "encrypted"
|
|
29
39
|
UNENCRYPTED = "unencrypted"
|
|
30
40
|
|
|
@@ -33,6 +43,8 @@ class AppliesToState(Enum):
|
|
|
33
43
|
|
|
34
44
|
|
|
35
45
|
class BindingMethod(Enum):
|
|
46
|
+
"""Assertion binding method enumeration."""
|
|
47
|
+
|
|
36
48
|
JWS = "jws"
|
|
37
49
|
|
|
38
50
|
def __str__(self):
|
|
@@ -40,7 +52,10 @@ class BindingMethod(Enum):
|
|
|
40
52
|
|
|
41
53
|
|
|
42
54
|
class AssertionKey:
|
|
55
|
+
"""Assertion signing key configuration."""
|
|
56
|
+
|
|
43
57
|
def __init__(self, alg: AssertionKeyAlg, key: Any):
|
|
58
|
+
"""Initialize assertion key."""
|
|
44
59
|
self.alg = alg
|
|
45
60
|
self.key = key
|
|
46
61
|
|
|
@@ -49,7 +64,10 @@ class AssertionKey:
|
|
|
49
64
|
|
|
50
65
|
|
|
51
66
|
class Statement:
|
|
67
|
+
"""Assertion statement with format, schema, and value."""
|
|
68
|
+
|
|
52
69
|
def __init__(self, format: str, schema: str, value: str):
|
|
70
|
+
"""Initialize assertion statement."""
|
|
53
71
|
self.format = format
|
|
54
72
|
self.schema = schema
|
|
55
73
|
self.value = value
|
|
@@ -67,6 +85,8 @@ class Statement:
|
|
|
67
85
|
|
|
68
86
|
|
|
69
87
|
class AssertionConfig:
|
|
88
|
+
"""TDF assertion configuration."""
|
|
89
|
+
|
|
70
90
|
def __init__(
|
|
71
91
|
self,
|
|
72
92
|
id: str,
|
|
@@ -76,6 +96,7 @@ class AssertionConfig:
|
|
|
76
96
|
statement: Statement,
|
|
77
97
|
signing_key: AssertionKey | None = None,
|
|
78
98
|
):
|
|
99
|
+
"""Initialize assertion configuration."""
|
|
79
100
|
self.id = id
|
|
80
101
|
self.type = type
|
|
81
102
|
self.scope = scope
|
otdf_python/asym_crypto.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Asymmetric encryption and decryption utilities for RSA keys in PEM format.
|
|
3
|
-
"""
|
|
1
|
+
"""Asymmetric encryption and decryption utilities for RSA keys in PEM format."""
|
|
4
2
|
|
|
5
3
|
import base64
|
|
6
4
|
import re
|
|
@@ -14,8 +12,7 @@ from .sdk_exceptions import SDKException
|
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
class AsymDecryption:
|
|
17
|
-
"""
|
|
18
|
-
Provides functionality for asymmetric decryption using an RSA private key.
|
|
15
|
+
"""Provides functionality for asymmetric decryption using an RSA private key.
|
|
19
16
|
|
|
20
17
|
Supports both PEM string and key object initialization for flexibility.
|
|
21
18
|
"""
|
|
@@ -25,8 +22,7 @@ class AsymDecryption:
|
|
|
25
22
|
PRIVATE_KEY_FOOTER = "-----END PRIVATE KEY-----"
|
|
26
23
|
|
|
27
24
|
def __init__(self, private_key_pem: str | None = None, private_key_obj=None):
|
|
28
|
-
"""
|
|
29
|
-
Initialize with either a PEM string or a key object.
|
|
25
|
+
"""Initialize with either a PEM string or a key object.
|
|
30
26
|
|
|
31
27
|
Args:
|
|
32
28
|
private_key_pem: Private key in PEM format (with or without headers)
|
|
@@ -34,6 +30,7 @@ class AsymDecryption:
|
|
|
34
30
|
|
|
35
31
|
Raises:
|
|
36
32
|
SDKException: If key loading fails
|
|
33
|
+
|
|
37
34
|
"""
|
|
38
35
|
if private_key_obj is not None:
|
|
39
36
|
self.private_key = private_key_obj
|
|
@@ -60,13 +57,12 @@ class AsymDecryption:
|
|
|
60
57
|
decoded, password=None, backend=default_backend()
|
|
61
58
|
)
|
|
62
59
|
except Exception as e:
|
|
63
|
-
raise SDKException(f"Failed to load private key: {e}")
|
|
60
|
+
raise SDKException(f"Failed to load private key: {e}") from e
|
|
64
61
|
else:
|
|
65
62
|
self.private_key = None
|
|
66
63
|
|
|
67
64
|
def decrypt(self, data: bytes) -> bytes:
|
|
68
|
-
"""
|
|
69
|
-
Decrypt data using RSA OAEP with SHA-1.
|
|
65
|
+
"""Decrypt data using RSA OAEP with SHA-1.
|
|
70
66
|
|
|
71
67
|
Args:
|
|
72
68
|
data: Encrypted bytes to decrypt
|
|
@@ -76,6 +72,7 @@ class AsymDecryption:
|
|
|
76
72
|
|
|
77
73
|
Raises:
|
|
78
74
|
SDKException: If decryption fails or key is not set
|
|
75
|
+
|
|
79
76
|
"""
|
|
80
77
|
if self.private_key is None:
|
|
81
78
|
raise SDKException("Failed to decrypt, private key is empty")
|
|
@@ -89,12 +86,11 @@ class AsymDecryption:
|
|
|
89
86
|
),
|
|
90
87
|
)
|
|
91
88
|
except Exception as e:
|
|
92
|
-
raise SDKException(f"Error performing decryption: {e}")
|
|
89
|
+
raise SDKException(f"Error performing decryption: {e}") from e
|
|
93
90
|
|
|
94
91
|
|
|
95
92
|
class AsymEncryption:
|
|
96
|
-
"""
|
|
97
|
-
Provides functionality for asymmetric encryption using an RSA public key or certificate in PEM format.
|
|
93
|
+
"""Provides functionality for asymmetric encryption using an RSA public key or certificate in PEM format.
|
|
98
94
|
|
|
99
95
|
Supports PEM public keys, X.509 certificates, and pre-loaded key objects.
|
|
100
96
|
Also handles base64-encoded keys without PEM headers.
|
|
@@ -105,8 +101,7 @@ class AsymEncryption:
|
|
|
105
101
|
CIPHER_TRANSFORM = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"
|
|
106
102
|
|
|
107
103
|
def __init__(self, public_key_pem: str | None = None, public_key_obj=None):
|
|
108
|
-
"""
|
|
109
|
-
Initialize with either a PEM string or a key object.
|
|
104
|
+
"""Initialize with either a PEM string or a key object.
|
|
110
105
|
|
|
111
106
|
Args:
|
|
112
107
|
public_key_pem: Public key in PEM format, X.509 certificate, or base64 string
|
|
@@ -114,6 +109,7 @@ class AsymEncryption:
|
|
|
114
109
|
|
|
115
110
|
Raises:
|
|
116
111
|
SDKException: If key loading fails or key is not RSA
|
|
112
|
+
|
|
117
113
|
"""
|
|
118
114
|
if public_key_obj is not None:
|
|
119
115
|
self.public_key = public_key_obj
|
|
@@ -141,7 +137,7 @@ class AsymEncryption:
|
|
|
141
137
|
decoded, backend=default_backend()
|
|
142
138
|
)
|
|
143
139
|
except Exception as e:
|
|
144
|
-
raise SDKException(f"Failed to load public key: {e}")
|
|
140
|
+
raise SDKException(f"Failed to load public key: {e}") from e
|
|
145
141
|
else:
|
|
146
142
|
self.public_key = None
|
|
147
143
|
|
|
@@ -152,8 +148,7 @@ class AsymEncryption:
|
|
|
152
148
|
raise SDKException("Not an RSA PEM formatted public key")
|
|
153
149
|
|
|
154
150
|
def encrypt(self, data: bytes) -> bytes:
|
|
155
|
-
"""
|
|
156
|
-
Encrypt data using RSA OAEP with SHA-1.
|
|
151
|
+
"""Encrypt data using RSA OAEP with SHA-1.
|
|
157
152
|
|
|
158
153
|
Args:
|
|
159
154
|
data: Plaintext bytes to encrypt
|
|
@@ -163,6 +158,7 @@ class AsymEncryption:
|
|
|
163
158
|
|
|
164
159
|
Raises:
|
|
165
160
|
SDKException: If encryption fails or key is not set
|
|
161
|
+
|
|
166
162
|
"""
|
|
167
163
|
if self.public_key is None:
|
|
168
164
|
raise SDKException("Failed to encrypt, public key is empty")
|
|
@@ -176,17 +172,17 @@ class AsymEncryption:
|
|
|
176
172
|
),
|
|
177
173
|
)
|
|
178
174
|
except Exception as e:
|
|
179
|
-
raise SDKException(f"Error performing encryption: {e}")
|
|
175
|
+
raise SDKException(f"Error performing encryption: {e}") from e
|
|
180
176
|
|
|
181
177
|
def public_key_in_pem_format(self) -> str:
|
|
182
|
-
"""
|
|
183
|
-
Export the public key to PEM format.
|
|
178
|
+
"""Export the public key to PEM format.
|
|
184
179
|
|
|
185
180
|
Returns:
|
|
186
181
|
Public key as PEM-encoded string
|
|
187
182
|
|
|
188
183
|
Raises:
|
|
189
184
|
SDKException: If export fails
|
|
185
|
+
|
|
190
186
|
"""
|
|
191
187
|
try:
|
|
192
188
|
pem = self.public_key.public_bytes(
|
|
@@ -195,4 +191,4 @@ class AsymEncryption:
|
|
|
195
191
|
)
|
|
196
192
|
return pem.decode()
|
|
197
193
|
except Exception as e:
|
|
198
|
-
raise SDKException(f"Error exporting public key to PEM: {e}")
|
|
194
|
+
raise SDKException(f"Error exporting public key to PEM: {e}") from e
|
otdf_python/auth_headers.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
"""Authentication header management."""
|
|
2
|
+
|
|
1
3
|
from dataclasses import dataclass
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
@dataclass
|
|
5
7
|
class AuthHeaders:
|
|
6
|
-
"""
|
|
7
|
-
Represents authentication headers used in token-based authorization.
|
|
8
|
+
"""Represents authentication headers used in token-based authorization.
|
|
8
9
|
This class holds authorization and DPoP (Demonstrating Proof of Possession) headers
|
|
9
10
|
that are used in token-based API requests.
|
|
10
11
|
"""
|
|
@@ -13,19 +14,19 @@ class AuthHeaders:
|
|
|
13
14
|
dpop_header: str = ""
|
|
14
15
|
|
|
15
16
|
def get_auth_header(self) -> str:
|
|
16
|
-
"""
|
|
17
|
+
"""Get the authorization header."""
|
|
17
18
|
return self.auth_header
|
|
18
19
|
|
|
19
20
|
def get_dpop_header(self) -> str:
|
|
20
|
-
"""
|
|
21
|
+
"""Get the DPoP header."""
|
|
21
22
|
return self.dpop_header
|
|
22
23
|
|
|
23
24
|
def to_dict(self) -> dict[str, str]:
|
|
24
|
-
"""
|
|
25
|
-
Convert authentication headers to a dictionary for use with HTTP clients.
|
|
25
|
+
"""Convert authentication headers to a dictionary for use with HTTP clients.
|
|
26
26
|
|
|
27
27
|
Returns:
|
|
28
28
|
Dictionary with 'Authorization' header and optionally 'DPoP' header
|
|
29
|
+
|
|
29
30
|
"""
|
|
30
31
|
headers = {"Authorization": self.auth_header}
|
|
31
32
|
if self.dpop_header:
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Utilities for automatic SDK configuration."""
|
|
2
|
+
|
|
1
3
|
import re
|
|
2
4
|
import urllib.parse
|
|
3
5
|
from dataclasses import dataclass
|
|
@@ -6,6 +8,8 @@ from typing import Any
|
|
|
6
8
|
|
|
7
9
|
# RuleType constants
|
|
8
10
|
class RuleType:
|
|
11
|
+
"""Rule type constants for attribute hierarchy."""
|
|
12
|
+
|
|
9
13
|
HIERARCHY = "hierarchy"
|
|
10
14
|
ALL_OF = "allOf"
|
|
11
15
|
ANY_OF = "anyOf"
|
|
@@ -15,6 +19,8 @@ class RuleType:
|
|
|
15
19
|
|
|
16
20
|
@dataclass(frozen=True)
|
|
17
21
|
class KeySplitStep:
|
|
22
|
+
"""Key split step information."""
|
|
23
|
+
|
|
18
24
|
kas: str
|
|
19
25
|
splitID: str
|
|
20
26
|
|
|
@@ -31,21 +37,26 @@ class KeySplitStep:
|
|
|
31
37
|
|
|
32
38
|
|
|
33
39
|
class AutoConfigureException(Exception):
|
|
40
|
+
"""Exception for auto-configuration errors."""
|
|
41
|
+
|
|
34
42
|
pass
|
|
35
43
|
|
|
36
44
|
|
|
37
45
|
class AttributeNameFQN:
|
|
46
|
+
"""Fully qualified attribute name."""
|
|
47
|
+
|
|
38
48
|
def __init__(self, url: str):
|
|
49
|
+
"""Initialize attribute name from URL."""
|
|
39
50
|
pattern = re.compile(r"^(https?://[\w./-]+)/attr/([^/\s]*)$")
|
|
40
51
|
matcher = pattern.match(url)
|
|
41
52
|
if not matcher or not matcher.group(1) or not matcher.group(2):
|
|
42
53
|
raise AutoConfigureException("invalid type: attribute regex fail")
|
|
43
54
|
try:
|
|
44
55
|
urllib.parse.unquote(matcher.group(2))
|
|
45
|
-
except Exception:
|
|
56
|
+
except Exception as err:
|
|
46
57
|
raise AutoConfigureException(
|
|
47
58
|
f"invalid type: error in attribute name [{matcher.group(2)}]"
|
|
48
|
-
)
|
|
59
|
+
) from err
|
|
49
60
|
self.url = url
|
|
50
61
|
self.key = url.lower()
|
|
51
62
|
|
|
@@ -76,12 +87,15 @@ class AttributeNameFQN:
|
|
|
76
87
|
raise AutoConfigureException("invalid attribute")
|
|
77
88
|
try:
|
|
78
89
|
return urllib.parse.unquote(matcher.group(1))
|
|
79
|
-
except Exception:
|
|
80
|
-
raise AutoConfigureException("invalid type")
|
|
90
|
+
except Exception as err:
|
|
91
|
+
raise AutoConfigureException("invalid type") from err
|
|
81
92
|
|
|
82
93
|
|
|
83
94
|
class AttributeValueFQN:
|
|
95
|
+
"""Fully qualified attribute value."""
|
|
96
|
+
|
|
84
97
|
def __init__(self, url: str):
|
|
98
|
+
"""Initialize attribute value from URL."""
|
|
85
99
|
pattern = re.compile(r"^(https?://[\w./-]+)/attr/(\S*)/value/(\S*)$")
|
|
86
100
|
matcher = pattern.match(url)
|
|
87
101
|
if (
|
|
@@ -96,8 +110,10 @@ class AttributeValueFQN:
|
|
|
96
110
|
try:
|
|
97
111
|
urllib.parse.unquote(matcher.group(2))
|
|
98
112
|
urllib.parse.unquote(matcher.group(3))
|
|
99
|
-
except Exception:
|
|
100
|
-
raise AutoConfigureException(
|
|
113
|
+
except Exception as err:
|
|
114
|
+
raise AutoConfigureException(
|
|
115
|
+
"invalid type: error in attribute or value"
|
|
116
|
+
) from err
|
|
101
117
|
self.url = url
|
|
102
118
|
self.key = url.lower()
|
|
103
119
|
|
otdf_python/cli.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
OpenTDF Python CLI
|
|
2
|
+
"""OpenTDF Python CLI.
|
|
4
3
|
|
|
5
4
|
A command-line interface for encrypting and decrypting files using OpenTDF.
|
|
6
5
|
Provides encrypt, decrypt, and inspect commands similar to the otdfctl CLI.
|
|
@@ -36,6 +35,7 @@ class CLIError(Exception):
|
|
|
36
35
|
"""Custom exception for CLI errors."""
|
|
37
36
|
|
|
38
37
|
def __init__(self, level: str, message: str, cause: Exception | None = None):
|
|
38
|
+
"""Initialize CLI error."""
|
|
39
39
|
self.level = level
|
|
40
40
|
self.message = message
|
|
41
41
|
self.cause = cause
|
|
@@ -105,11 +105,11 @@ def load_client_credentials(creds_file_path: str) -> tuple[str, str]:
|
|
|
105
105
|
except json.JSONDecodeError as e:
|
|
106
106
|
raise CLIError(
|
|
107
107
|
"CRITICAL", f"Invalid JSON in credentials file {creds_file_path}: {e}"
|
|
108
|
-
)
|
|
108
|
+
) from e
|
|
109
109
|
except Exception as e:
|
|
110
110
|
raise CLIError(
|
|
111
111
|
"CRITICAL", f"Error reading credentials file {creds_file_path}: {e}"
|
|
112
|
-
)
|
|
112
|
+
) from e
|
|
113
113
|
|
|
114
114
|
|
|
115
115
|
def build_sdk(args) -> SDK:
|
|
@@ -525,7 +525,7 @@ Where creds.json contains:
|
|
|
525
525
|
|
|
526
526
|
|
|
527
527
|
def main():
|
|
528
|
-
"""
|
|
528
|
+
"""Execute the CLI entry point."""
|
|
529
529
|
parser = create_parser()
|
|
530
530
|
args = parser.parse_args()
|
|
531
531
|
|
otdf_python/collection_store.py
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
+
"""Collection store interface for managing collections."""
|
|
2
|
+
|
|
1
3
|
from collections import OrderedDict
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
class CollectionKey:
|
|
7
|
+
"""Collection key wrapper for store operations."""
|
|
8
|
+
|
|
5
9
|
def __init__(self, key: bytes | None):
|
|
10
|
+
"""Initialize collection key."""
|
|
6
11
|
self.key = key
|
|
7
12
|
|
|
8
13
|
|
|
9
14
|
class CollectionStore:
|
|
15
|
+
"""Abstract collection store interface for key management."""
|
|
16
|
+
|
|
10
17
|
NO_PRIVATE_KEY = CollectionKey(None)
|
|
11
18
|
|
|
12
19
|
def store(self, header, key: CollectionKey):
|
|
@@ -17,7 +24,10 @@ class CollectionStore:
|
|
|
17
24
|
|
|
18
25
|
|
|
19
26
|
class NoOpCollectionStore(CollectionStore):
|
|
27
|
+
"""No-op collection store that discards all keys."""
|
|
28
|
+
|
|
20
29
|
def store(self, header, key: CollectionKey):
|
|
30
|
+
"""Discard key operation (no-op)."""
|
|
21
31
|
pass
|
|
22
32
|
|
|
23
33
|
def get_key(self, header) -> CollectionKey:
|
|
@@ -25,9 +35,12 @@ class NoOpCollectionStore(CollectionStore):
|
|
|
25
35
|
|
|
26
36
|
|
|
27
37
|
class CollectionStoreImpl(OrderedDict, CollectionStore):
|
|
38
|
+
"""Collection store implementation with ordered dictionary."""
|
|
39
|
+
|
|
28
40
|
MAX_SIZE_STORE = 500
|
|
29
41
|
|
|
30
42
|
def __init__(self):
|
|
43
|
+
"""Initialize collection store."""
|
|
31
44
|
super().__init__()
|
|
32
45
|
|
|
33
46
|
def store(self, header, key: CollectionKey):
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Collection store implementation."""
|
|
2
|
+
|
|
1
3
|
from collections import OrderedDict
|
|
2
4
|
from threading import RLock
|
|
3
5
|
|
|
@@ -5,7 +7,10 @@ MAX_SIZE_STORE = 500
|
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
class CollectionStoreImpl(OrderedDict):
|
|
10
|
+
"""Thread-safe collection store for caching TDF keys."""
|
|
11
|
+
|
|
8
12
|
def __init__(self):
|
|
13
|
+
"""Initialize collection store."""
|
|
9
14
|
super().__init__()
|
|
10
15
|
self._lock = RLock()
|
|
11
16
|
|
otdf_python/config.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Configuration classes for TDF and NanoTDF operations."""
|
|
2
|
+
|
|
1
3
|
from dataclasses import dataclass, field
|
|
2
4
|
from enum import Enum
|
|
3
5
|
from typing import Any
|
|
@@ -5,17 +7,23 @@ from urllib.parse import urlparse, urlunparse
|
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
class TDFFormat(Enum):
|
|
10
|
+
"""TDF format enumeration."""
|
|
11
|
+
|
|
8
12
|
JSONFormat = "JSONFormat"
|
|
9
13
|
XMLFormat = "XMLFormat"
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
class IntegrityAlgorithm(Enum):
|
|
17
|
+
"""Integrity algorithm enumeration."""
|
|
18
|
+
|
|
13
19
|
HS256 = "HS256"
|
|
14
20
|
GMAC = "GMAC"
|
|
15
21
|
|
|
16
22
|
|
|
17
23
|
@dataclass
|
|
18
24
|
class KASInfo:
|
|
25
|
+
"""Key Access Service information."""
|
|
26
|
+
|
|
19
27
|
url: str
|
|
20
28
|
public_key: str | None = None
|
|
21
29
|
kid: str | None = None
|
|
@@ -28,6 +36,8 @@ class KASInfo:
|
|
|
28
36
|
|
|
29
37
|
@dataclass
|
|
30
38
|
class TDFConfig:
|
|
39
|
+
"""TDF encryption configuration."""
|
|
40
|
+
|
|
31
41
|
autoconfigure: bool = True
|
|
32
42
|
default_segment_size: int = 2 * 1024 * 1024
|
|
33
43
|
enable_encryption: bool = True
|
|
@@ -49,6 +59,8 @@ class TDFConfig:
|
|
|
49
59
|
|
|
50
60
|
@dataclass
|
|
51
61
|
class NanoTDFConfig:
|
|
62
|
+
"""NanoTDF encryption configuration."""
|
|
63
|
+
|
|
52
64
|
ecc_mode: str | None = None
|
|
53
65
|
cipher: str | None = None
|
|
54
66
|
config: str | None = None
|
|
@@ -60,6 +72,7 @@ class NanoTDFConfig:
|
|
|
60
72
|
|
|
61
73
|
# Utility function to normalize KAS URLs (Python equivalent)
|
|
62
74
|
def get_kas_address(kas_url: str) -> str:
|
|
75
|
+
"""Normalize KAS URL by adding https:// if no scheme present."""
|
|
63
76
|
if "://" not in kas_url:
|
|
64
77
|
kas_url = "https://" + kas_url
|
|
65
78
|
parsed = urlparse(kas_url)
|
otdf_python/connect_client.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Connect RPC client for KAS operations."""
|
otdf_python/constants.py
CHANGED
otdf_python/crypto_utils.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Cryptographic utility functions."""
|
|
2
|
+
|
|
1
3
|
import hashlib
|
|
2
4
|
import hmac
|
|
3
5
|
|
|
@@ -7,6 +9,8 @@ from cryptography.hazmat.primitives.asymmetric import ec, rsa
|
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class CryptoUtils:
|
|
12
|
+
"""Cryptographic utility functions and helpers."""
|
|
13
|
+
|
|
10
14
|
KEYPAIR_SIZE = 2048
|
|
11
15
|
|
|
12
16
|
@staticmethod
|
otdf_python/dpop.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
DPoP (Demonstration of Proof-of-Possession) token generation utilities.
|
|
3
|
-
"""
|
|
1
|
+
"""DPoP (Demonstration of Proof-of-Possession) token generation utilities."""
|
|
4
2
|
|
|
5
3
|
import base64
|
|
6
4
|
import hashlib
|
|
@@ -18,8 +16,7 @@ def create_dpop_token(
|
|
|
18
16
|
method: str = "POST",
|
|
19
17
|
access_token: str | None = None,
|
|
20
18
|
) -> str:
|
|
21
|
-
"""
|
|
22
|
-
Create a DPoP (Demonstration of Proof-of-Possession) token.
|
|
19
|
+
"""Create a DPoP (Demonstration of Proof-of-Possession) token.
|
|
23
20
|
|
|
24
21
|
Args:
|
|
25
22
|
private_key_pem: RSA private key in PEM format for signing
|
|
@@ -30,6 +27,7 @@ def create_dpop_token(
|
|
|
30
27
|
|
|
31
28
|
Returns:
|
|
32
29
|
DPoP token as a string
|
|
30
|
+
|
|
33
31
|
"""
|
|
34
32
|
# Parse the RSA public key to extract modulus and exponent
|
|
35
33
|
public_key_obj = CryptoUtils.get_rsa_public_key_from_pem(public_key_pem)
|