pysealer 0.2.1__cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.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.
pysealer/__init__.py ADDED
@@ -0,0 +1,24 @@
1
+ """Pysealer package entry point
2
+
3
+ This package serves as a bridge between the Python command line interface and the
4
+ underlying Rust implementation (compiled as _pysealer module). It exposes the
5
+ core functionality for adding version control decorators to Python functions.
6
+
7
+ This module also dynamically provides decorator placeholders (e.g. @pysealer._<sig>)
8
+ so that decorated functions remain importable.
9
+ """
10
+
11
+ # Define the rust to python module version and functions
12
+ from ._pysealer import generate_keypair, generate_signature, verify_signature
13
+
14
+ __version__ = "0.2.1"
15
+ __all__ = ["generate_keypair", "generate_signature", "verify_signature"]
16
+
17
+ # Ensure dummy decorators are registered on import
18
+ from . import dummy_decorators
19
+
20
+ # Allow dynamic decorator resolution for @pyseal._<sig>()
21
+ def __getattr__(name):
22
+ if name.startswith("_"):
23
+ return dummy_decorators._dummy_decorator
24
+ raise AttributeError(f"module 'pysealer' has no attribute '{name}'")
@@ -0,0 +1,215 @@
1
+ """Automatically add cryptographic decorators to all functions and classes in a python file."""
2
+
3
+ import ast
4
+ import copy
5
+ from pathlib import Path
6
+ from pysealer import generate_signature
7
+ from .setup import get_private_key
8
+
9
+ def add_decorators(file_path: str) -> str:
10
+ """
11
+ Parse a Python file, add decorators to all functions and classes, and return the modified code.
12
+
13
+ Args:
14
+ file_path: Path to the Python file to process
15
+
16
+ Returns:
17
+ Modified Python source code as a string
18
+ """
19
+ # Read the entire file content into a string
20
+ with open(file_path, 'r') as f:
21
+ content = f.read()
22
+
23
+ # Split content into lines for manipulation
24
+ lines = content.split('\n')
25
+
26
+ # Dynamically add 'import pysealer' grouped with other imports if not present
27
+ has_import_pysealer = any(
28
+ line.strip() == 'import pysealer' or line.strip().startswith('import pysealer') or line.strip().startswith('from pysealer')
29
+ for line in lines
30
+ )
31
+ if not has_import_pysealer:
32
+ # Find the import block
33
+ import_indices = [i for i, line in enumerate(lines) if line.strip().startswith('import ') or line.strip().startswith('from ')]
34
+ if import_indices:
35
+ # Insert after the last import in the block
36
+ last_import = import_indices[-1]
37
+ lines.insert(last_import + 1, 'import pysealer')
38
+ else:
39
+ # No import block found, insert after shebang/docstring/comments as before
40
+ insert_at = 0
41
+ if lines and lines[0].startswith('#!'):
42
+ insert_at = 1
43
+ while insert_at < len(lines) and (lines[insert_at].strip() == '' or lines[insert_at].strip().startswith('"""') or lines[insert_at].strip().startswith("''")):
44
+ if lines[insert_at].strip().startswith('"""') or lines[insert_at].strip().startswith("''"):
45
+ quote = lines[insert_at].strip()[:3]
46
+ insert_at += 1
47
+ while insert_at < len(lines) and not lines[insert_at].strip().endswith(quote):
48
+ insert_at += 1
49
+ if insert_at < len(lines):
50
+ insert_at += 1
51
+ else:
52
+ insert_at += 1
53
+ lines.insert(insert_at, 'import pysealer')
54
+
55
+ # Parse the Python source code into an Abstract Syntax Tree (AST)
56
+ content = '\n'.join(lines)
57
+ tree = ast.parse(content)
58
+
59
+ # First pass: Remove existing pysealer decorators
60
+ lines_to_remove = set()
61
+ for node in ast.walk(tree):
62
+ if type(node).__name__ in ("FunctionDef", "AsyncFunctionDef", "ClassDef"):
63
+ if hasattr(node, 'decorator_list'):
64
+ for decorator in node.decorator_list:
65
+ is_pysealer_decorator = False
66
+
67
+ if isinstance(decorator, ast.Name):
68
+ if decorator.id.startswith("pysealer"):
69
+ is_pysealer_decorator = True
70
+ elif isinstance(decorator, ast.Attribute):
71
+ if isinstance(decorator.value, ast.Name) and decorator.value.id == "pysealer":
72
+ is_pysealer_decorator = True
73
+ elif isinstance(decorator, ast.Call):
74
+ func = decorator.func
75
+ if isinstance(func, ast.Attribute):
76
+ if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
77
+ is_pysealer_decorator = True
78
+ elif isinstance(func, ast.Name) and func.id.startswith("pysealer"):
79
+ is_pysealer_decorator = True
80
+
81
+ if is_pysealer_decorator:
82
+ # Mark this line for removal (convert to 0-indexed)
83
+ lines_to_remove.add(decorator.lineno - 1)
84
+
85
+ # Remove the marked lines (in reverse order to preserve indices)
86
+ for line_idx in sorted(lines_to_remove, reverse=True):
87
+ del lines[line_idx]
88
+
89
+ # Re-parse the content after removing decorators to get updated line numbers
90
+ modified_content = '\n'.join(lines)
91
+ tree = ast.parse(modified_content)
92
+
93
+ # Build parent map for all nodes
94
+ parent_map = {}
95
+ for parent in ast.walk(tree):
96
+ for child in ast.iter_child_nodes(parent):
97
+ parent_map[child] = parent
98
+
99
+ decorators_to_add = []
100
+
101
+ for node in ast.walk(tree):
102
+ node_type = type(node).__name__
103
+
104
+ # Only decorate:
105
+ # - Top-level functions (not inside a class)
106
+ # - Top-level classes
107
+ if node_type in ("FunctionDef", "AsyncFunctionDef"):
108
+ parent = parent_map.get(node)
109
+ if isinstance(parent, ast.ClassDef):
110
+ continue # skip methods inside classes
111
+ elif node_type == "ClassDef":
112
+ pass # always decorate classes
113
+ else:
114
+ continue
115
+
116
+ # Extract the complete source code of this function/class for hashing
117
+ node_clone = copy.deepcopy(node)
118
+
119
+ # Filter out pysealer decorators
120
+ if hasattr(node_clone, 'decorator_list'):
121
+ filtered_decorators = []
122
+ for decorator in node_clone.decorator_list:
123
+ should_keep = True
124
+ if isinstance(decorator, ast.Name):
125
+ if decorator.id.startswith("pysealer"):
126
+ should_keep = False
127
+ elif isinstance(decorator, ast.Attribute):
128
+ if isinstance(decorator.value, ast.Name) and decorator.value.id == "pysealer":
129
+ should_keep = False
130
+ elif isinstance(decorator, ast.Call):
131
+ func = decorator.func
132
+ if isinstance(func, ast.Attribute):
133
+ if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
134
+ should_keep = False
135
+ elif isinstance(func, ast.Name) and func.id.startswith("pysealer"):
136
+ should_keep = False
137
+ if should_keep:
138
+ filtered_decorators.append(decorator)
139
+ node_clone.decorator_list = filtered_decorators
140
+
141
+ module_wrapper = ast.Module(body=[node_clone], type_ignores=[])
142
+ function_source = ast.unparse(module_wrapper)
143
+
144
+ try:
145
+ private_key = get_private_key()
146
+ except (FileNotFoundError, ValueError) as e:
147
+ raise RuntimeError(f"Cannot add decorators: {e}. Please run 'pysealer init' first.")
148
+
149
+ try:
150
+ signature = generate_signature(function_source, private_key)
151
+ except Exception as e:
152
+ raise RuntimeError(f"Failed to generate signature: {e}")
153
+
154
+ decorator_line = node.lineno - 1
155
+ if hasattr(node, 'decorator_list') and node.decorator_list:
156
+ decorator_line = node.decorator_list[0].lineno - 1
157
+
158
+ decorators_to_add.append((decorator_line, node.col_offset, signature))
159
+
160
+ # Sort in reverse order to add from bottom to top (preserves line numbers)
161
+ decorators_to_add.sort(reverse=True)
162
+
163
+ # Add decorators to the lines
164
+ for line_idx, col_offset, signature in decorators_to_add:
165
+ indent = ' ' * col_offset
166
+ decorator_line = f"{indent}@pysealer._{signature}()"
167
+ lines.insert(line_idx, decorator_line)
168
+
169
+ # Join lines back together
170
+ modified_code = '\n'.join(lines)
171
+
172
+ return modified_code
173
+
174
+
175
+ def add_decorators_to_folder(folder_path: str) -> list[str]:
176
+ """
177
+ Add decorators to all Python files in a folder.
178
+
179
+ Args:
180
+ folder_path: Path to the folder containing Python files
181
+
182
+ Returns:
183
+ List of file paths that were successfully decorated
184
+ """
185
+ folder = Path(folder_path)
186
+
187
+ if not folder.exists():
188
+ raise FileNotFoundError(f"Folder '{folder_path}' does not exist.")
189
+
190
+ if not folder.is_dir():
191
+ raise NotADirectoryError(f"'{folder_path}' is not a directory.")
192
+
193
+ # Find all Python files in the folder (non-recursive)
194
+ python_files = list(folder.glob('*.py'))
195
+
196
+ if not python_files:
197
+ raise ValueError(f"No Python files found in '{folder_path}'.")
198
+
199
+ decorated_files = []
200
+ errors = []
201
+
202
+ for py_file in python_files:
203
+ try:
204
+ modified_code = add_decorators(str(py_file))
205
+ with open(py_file, 'w') as f:
206
+ f.write(modified_code)
207
+ decorated_files.append(str(py_file))
208
+ except Exception as e:
209
+ errors.append((str(py_file), str(e)))
210
+
211
+ if errors:
212
+ error_msg = "\n".join([f" - {file}: {error}" for file, error in errors])
213
+ raise RuntimeError(f"Failed to decorate some files:\n{error_msg}")
214
+
215
+ return decorated_files
@@ -0,0 +1,179 @@
1
+ """Automatically verify cryptographic decorators for all functions and classes in a python file."""
2
+
3
+ import ast
4
+ import copy
5
+ from pathlib import Path
6
+ from typing import Dict
7
+ from pysealer import verify_signature
8
+ from .setup import get_public_key
9
+
10
+
11
+ def check_decorators(file_path: str) -> Dict[str, dict]:
12
+ """
13
+ Parse a Python file and verify all pysealer cryptographic decorators.
14
+
15
+ This function checks that each function/class with a pysealer decorator has a valid
16
+ signature that matches the current source code of that function/class.
17
+
18
+ Args:
19
+ file_path: Path to the Python file to verify
20
+
21
+ Returns:
22
+ Dictionary mapping function/class names to their verification results:
23
+ {
24
+ "function_name": {
25
+ "valid": bool, # Whether signature is valid
26
+ "signature": str, # The signature found in decorator
27
+ "message": str, # Success or error message
28
+ "has_decorator": bool # Whether function has pysealer decorator
29
+ }
30
+ }
31
+ """
32
+ # Read the file content
33
+ with open(file_path, 'r') as f:
34
+ content = f.read()
35
+
36
+ # Parse the Python source code into an AST
37
+ tree = ast.parse(content)
38
+
39
+ # Get the public key for verification
40
+ try:
41
+ public_key = get_public_key()
42
+ except (FileNotFoundError, ValueError) as e:
43
+ raise RuntimeError(f"Cannot verify decorators: {e}. Please run 'pysealer init' first.")
44
+
45
+ # Dictionary to store results
46
+ results = {}
47
+
48
+ # Iterate through each node in the AST
49
+ for node in ast.walk(tree):
50
+ node_type = type(node).__name__
51
+
52
+ # Check if this node is a function or class definition
53
+ if node_type in ("FunctionDef", "AsyncFunctionDef", "ClassDef"):
54
+ name = node.name
55
+
56
+ # Look for pysealer decorator
57
+ signature_from_decorator = None
58
+ has_pysealer_decorator = False
59
+
60
+ if hasattr(node, 'decorator_list'):
61
+ for decorator in node.decorator_list:
62
+ # Check if decorator is a Call node (e.g., @pysealer._<signature>())
63
+ if isinstance(decorator, ast.Call):
64
+ func = decorator.func
65
+ # Check if it's pysealer._<signature>
66
+ if isinstance(func, ast.Attribute):
67
+ if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
68
+ attr_name = func.attr
69
+ # Extract signature (remove leading underscore)
70
+ if attr_name.startswith('_'):
71
+ signature_from_decorator = attr_name[1:]
72
+ has_pysealer_decorator = True
73
+ break
74
+
75
+ # Initialize result for this function/class
76
+ result = {
77
+ "has_decorator": has_pysealer_decorator,
78
+ "valid": False,
79
+ "signature": signature_from_decorator,
80
+ "message": ""
81
+ }
82
+
83
+ if not has_pysealer_decorator:
84
+ result["message"] = "No pysealer decorator found"
85
+ results[name] = result
86
+ continue
87
+
88
+ # Extract the source code without pysealer decorators for verification
89
+ node_clone = copy.deepcopy(node)
90
+
91
+ # Filter out pysealer decorators from the clone
92
+ if hasattr(node_clone, 'decorator_list'):
93
+ filtered_decorators = []
94
+
95
+ for decorator in node_clone.decorator_list:
96
+ should_keep = True
97
+
98
+ # Check if decorator is a simple Name node starting with "pysealer"
99
+ if isinstance(decorator, ast.Name):
100
+ if decorator.id.startswith("pysealer"):
101
+ should_keep = False
102
+
103
+ # Check if decorator is an Attribute node (e.g., pysealer.something)
104
+ elif isinstance(decorator, ast.Attribute):
105
+ if isinstance(decorator.value, ast.Name) and decorator.value.id == "pysealer":
106
+ should_keep = False
107
+
108
+ # Check if decorator is a Call node
109
+ elif isinstance(decorator, ast.Call):
110
+ func = decorator.func
111
+ # Check if call is to pysealer.something()
112
+ if isinstance(func, ast.Attribute):
113
+ if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
114
+ should_keep = False
115
+ # Check if call is to pysealer_something()
116
+ elif isinstance(func, ast.Name) and func.id.startswith("pysealer"):
117
+ should_keep = False
118
+
119
+ if should_keep:
120
+ filtered_decorators.append(decorator)
121
+
122
+ node_clone.decorator_list = filtered_decorators
123
+
124
+ # Convert the filtered node back to source code
125
+ module_wrapper = ast.Module(body=[node_clone], type_ignores=[])
126
+ function_source = ast.unparse(module_wrapper)
127
+
128
+ # Verify the signature
129
+ try:
130
+ is_valid = verify_signature(function_source, signature_from_decorator, public_key)
131
+
132
+ result["valid"] = is_valid
133
+ if is_valid:
134
+ result["message"] = "✓ Signature valid - code has not been tampered with"
135
+ else:
136
+ result["message"] = "✗ Signature invalid - code may have been modified"
137
+
138
+ except Exception as e:
139
+ result["message"] = f"✗ Error verifying signature: {e}"
140
+
141
+ results[name] = result
142
+
143
+ return results
144
+
145
+
146
+ def check_decorators_in_folder(folder_path: str) -> Dict[str, Dict[str, dict]]:
147
+ """
148
+ Check decorators in all Python files in a folder.
149
+
150
+ Args:
151
+ folder_path: Path to the folder containing Python files
152
+
153
+ Returns:
154
+ Dictionary mapping file paths to their verification results
155
+ """
156
+ folder = Path(folder_path)
157
+
158
+ if not folder.exists():
159
+ raise FileNotFoundError(f"Folder '{folder_path}' does not exist.")
160
+
161
+ if not folder.is_dir():
162
+ raise NotADirectoryError(f"'{folder_path}' is not a directory.")
163
+
164
+ # Find all Python files in the folder (non-recursive)
165
+ python_files = list(folder.glob('*.py'))
166
+
167
+ if not python_files:
168
+ raise ValueError(f"No Python files found in '{folder_path}'.")
169
+
170
+ all_results = {}
171
+
172
+ for py_file in python_files:
173
+ try:
174
+ results = check_decorators(str(py_file))
175
+ all_results[str(py_file)] = results
176
+ except Exception as e:
177
+ all_results[str(py_file)] = {"error": str(e)}
178
+
179
+ return all_results