pysealer 0.4.1__cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.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.4.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,237 @@
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) -> tuple[str, bool]:
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
+ Tuple of (modified Python source code as a string, whether any decorators were added)
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
+ # Parse the Python source code into an Abstract Syntax Tree (AST)
27
+ tree = ast.parse(content)
28
+
29
+ # First pass: Remove existing pysealer decorators
30
+ lines_to_remove = set()
31
+ for node in ast.walk(tree):
32
+ if type(node).__name__ in ("FunctionDef", "AsyncFunctionDef", "ClassDef"):
33
+ if hasattr(node, 'decorator_list'):
34
+ for decorator in node.decorator_list:
35
+ is_pysealer_decorator = False
36
+
37
+ if isinstance(decorator, ast.Name):
38
+ if decorator.id.startswith("pysealer"):
39
+ is_pysealer_decorator = True
40
+ elif isinstance(decorator, ast.Attribute):
41
+ if isinstance(decorator.value, ast.Name) and decorator.value.id == "pysealer":
42
+ is_pysealer_decorator = True
43
+ elif isinstance(decorator, ast.Call):
44
+ func = decorator.func
45
+ if isinstance(func, ast.Attribute):
46
+ if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
47
+ is_pysealer_decorator = True
48
+ elif isinstance(func, ast.Name) and func.id.startswith("pysealer"):
49
+ is_pysealer_decorator = True
50
+
51
+ if is_pysealer_decorator:
52
+ # Mark this line for removal (convert to 0-indexed)
53
+ lines_to_remove.add(decorator.lineno - 1)
54
+
55
+ # Remove the marked lines (in reverse order to preserve indices)
56
+ for line_idx in sorted(lines_to_remove, reverse=True):
57
+ del lines[line_idx]
58
+
59
+ # Re-parse the content after removing decorators to get updated line numbers
60
+ modified_content = '\n'.join(lines)
61
+ tree = ast.parse(modified_content)
62
+
63
+ # Build parent map for all nodes
64
+ parent_map = {}
65
+ for parent in ast.walk(tree):
66
+ for child in ast.iter_child_nodes(parent):
67
+ parent_map[child] = parent
68
+
69
+ decorators_to_add = []
70
+
71
+ for node in ast.walk(tree):
72
+ node_type = type(node).__name__
73
+
74
+ # Only decorate:
75
+ # - Top-level functions (not inside a class)
76
+ # - Top-level classes
77
+ if node_type in ("FunctionDef", "AsyncFunctionDef"):
78
+ parent = parent_map.get(node)
79
+ if isinstance(parent, ast.ClassDef):
80
+ continue # skip methods inside classes
81
+ elif node_type == "ClassDef":
82
+ pass # always decorate classes
83
+ else:
84
+ continue
85
+
86
+ # Extract the complete source code of this function/class for hashing
87
+ node_clone = copy.deepcopy(node)
88
+
89
+ # Filter out pysealer decorators
90
+ if hasattr(node_clone, 'decorator_list'):
91
+ filtered_decorators = []
92
+ for decorator in node_clone.decorator_list:
93
+ should_keep = True
94
+ if isinstance(decorator, ast.Name):
95
+ if decorator.id.startswith("pysealer"):
96
+ should_keep = False
97
+ elif isinstance(decorator, ast.Attribute):
98
+ if isinstance(decorator.value, ast.Name) and decorator.value.id == "pysealer":
99
+ should_keep = False
100
+ elif isinstance(decorator, ast.Call):
101
+ func = decorator.func
102
+ if isinstance(func, ast.Attribute):
103
+ if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
104
+ should_keep = False
105
+ elif isinstance(func, ast.Name) and func.id.startswith("pysealer"):
106
+ should_keep = False
107
+ if should_keep:
108
+ filtered_decorators.append(decorator)
109
+ node_clone.decorator_list = filtered_decorators
110
+
111
+ module_wrapper = ast.Module(body=[node_clone], type_ignores=[])
112
+ function_source = ast.unparse(module_wrapper)
113
+
114
+ try:
115
+ private_key = get_private_key()
116
+ except (FileNotFoundError, ValueError) as e:
117
+ raise RuntimeError(f"Cannot add decorators: {e}. Please run 'pysealer init' first.")
118
+
119
+ try:
120
+ signature = generate_signature(function_source, private_key)
121
+ except Exception as e:
122
+ raise RuntimeError(f"Failed to generate signature: {e}")
123
+
124
+ decorator_line = node.lineno - 1
125
+ if hasattr(node, 'decorator_list') and node.decorator_list:
126
+ decorator_line = node.decorator_list[0].lineno - 1
127
+
128
+ decorators_to_add.append((decorator_line, node.col_offset, signature))
129
+
130
+ # If no decorators to add, return original content
131
+ if not decorators_to_add:
132
+ return content, False
133
+
134
+ # Sort in reverse order to add from bottom to top (preserves line numbers)
135
+ decorators_to_add.sort(reverse=True)
136
+
137
+ # Add decorators to the lines first
138
+ for line_idx, col_offset, signature in decorators_to_add:
139
+ indent = ' ' * col_offset
140
+ decorator_line = f"{indent}@pysealer._{signature}()"
141
+ lines.insert(line_idx, decorator_line)
142
+
143
+ # Now add 'import pysealer' at the top if not present
144
+ has_import_pysealer = any(
145
+ line.strip() == 'import pysealer' or line.strip().startswith('import pysealer') or line.strip().startswith('from pysealer')
146
+ for line in lines
147
+ )
148
+ if not has_import_pysealer:
149
+ # Find the import block
150
+ import_indices = [i for i, line in enumerate(lines) if line.strip().startswith('import ') or line.strip().startswith('from ')]
151
+ if import_indices:
152
+ # Insert after the last import in the block
153
+ last_import = import_indices[-1]
154
+ lines.insert(last_import + 1, 'import pysealer')
155
+ else:
156
+ # No import block found, insert after shebang/docstring/comments
157
+ insert_at = 0
158
+ if lines and lines[0].startswith('#!'):
159
+ insert_at = 1
160
+ # Skip module-level docstrings and blank lines
161
+ while insert_at < len(lines):
162
+ line = lines[insert_at].strip()
163
+ if line == '':
164
+ insert_at += 1
165
+ elif line.startswith('"""') or line.startswith("'''"):
166
+ # Handle multi-line docstrings
167
+ quote = '"""' if line.startswith('"""') else "'''"
168
+ # Check if docstring ends on same line
169
+ if line.count(quote) >= 2:
170
+ insert_at += 1
171
+ else:
172
+ # Multi-line docstring
173
+ insert_at += 1
174
+ while insert_at < len(lines) and quote not in lines[insert_at]:
175
+ insert_at += 1
176
+ if insert_at < len(lines):
177
+ insert_at += 1
178
+ elif line.startswith('#'):
179
+ # Skip comments
180
+ insert_at += 1
181
+ else:
182
+ # Found first non-blank, non-comment, non-docstring line
183
+ break
184
+
185
+ lines.insert(insert_at, 'import pysealer')
186
+ # Add blank line after import if the next line isn't blank
187
+ if insert_at + 1 < len(lines) and lines[insert_at + 1].strip() != '':
188
+ lines.insert(insert_at + 1, '')
189
+
190
+ # Join lines back together
191
+ modified_code = '\n'.join(lines)
192
+
193
+ return modified_code, True
194
+
195
+
196
+ def add_decorators_to_folder(folder_path: str) -> list[str]:
197
+ """
198
+ Add decorators to all Python files in a folder.
199
+
200
+ Args:
201
+ folder_path: Path to the folder containing Python files
202
+
203
+ Returns:
204
+ List of file paths that were successfully decorated
205
+ """
206
+ folder = Path(folder_path)
207
+
208
+ if not folder.exists():
209
+ raise FileNotFoundError(f"Folder '{folder_path}' does not exist.")
210
+
211
+ if not folder.is_dir():
212
+ raise NotADirectoryError(f"'{folder_path}' is not a directory.")
213
+
214
+ # Find all Python files in the folder (non-recursive)
215
+ python_files = list(folder.glob('*.py'))
216
+
217
+ if not python_files:
218
+ raise ValueError(f"No Python files found in '{folder_path}'.")
219
+
220
+ decorated_files = []
221
+ errors = []
222
+
223
+ for py_file in python_files:
224
+ try:
225
+ modified_code, has_changes = add_decorators(str(py_file))
226
+ if has_changes:
227
+ with open(py_file, 'w') as f:
228
+ f.write(modified_code)
229
+ decorated_files.append(str(py_file))
230
+ except Exception as e:
231
+ errors.append((str(py_file), str(e)))
232
+
233
+ if errors:
234
+ error_msg = "\n".join([f" - {file}: {error}" for file, error in errors])
235
+ raise RuntimeError(f"Failed to decorate some files:\n{error_msg}")
236
+
237
+ return decorated_files
@@ -0,0 +1,205 @@
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, List, Tuple, Optional
7
+ from pysealer import verify_signature
8
+ from .setup import get_public_key
9
+ from .git_diff import get_function_diff
10
+
11
+
12
+ def check_decorators(file_path: str) -> Dict[str, dict]:
13
+ """
14
+ Parse a Python file and verify all pysealer cryptographic decorators.
15
+
16
+ This function checks that each function/class with a pysealer decorator has a valid
17
+ signature that matches the current source code of that function/class.
18
+
19
+ Args:
20
+ file_path: Path to the Python file to verify
21
+
22
+ Returns:
23
+ Dictionary mapping function/class names to their verification results:
24
+ {
25
+ "function_name": {
26
+ "valid": bool, # Whether signature is valid
27
+ "signature": str, # The signature found in decorator
28
+ "message": str, # Success or error message
29
+ "has_decorator": bool, # Whether function has pysealer decorator
30
+ "line_start": int, # Starting line number
31
+ "line_end": int, # Ending line number
32
+ "source": str, # Function source code
33
+ "diff": List[Tuple] # Git diff if validation failed
34
+ }
35
+ }
36
+ """
37
+ # Read the file content
38
+ with open(file_path, 'r') as f:
39
+ content = f.read()
40
+
41
+ # Parse the Python source code into an AST
42
+ tree = ast.parse(content)
43
+
44
+ # Get the public key for verification
45
+ try:
46
+ public_key = get_public_key()
47
+ except (FileNotFoundError, ValueError) as e:
48
+ raise RuntimeError(f"Cannot verify decorators: {e}. Please run 'pysealer init' first.")
49
+
50
+ # Dictionary to store results
51
+ results = {}
52
+
53
+ # Iterate through each node in the AST
54
+ for node in ast.walk(tree):
55
+ node_type = type(node).__name__
56
+
57
+ # Check if this node is a function or class definition
58
+ if node_type in ("FunctionDef", "AsyncFunctionDef", "ClassDef"):
59
+ name = node.name
60
+
61
+ # Look for pysealer decorator
62
+ signature_from_decorator = None
63
+ has_pysealer_decorator = False
64
+
65
+ if hasattr(node, 'decorator_list'):
66
+ for decorator in node.decorator_list:
67
+ # Check if decorator is a Call node (e.g., @pysealer._<signature>())
68
+ if isinstance(decorator, ast.Call):
69
+ func = decorator.func
70
+ # Check if it's pysealer._<signature>
71
+ if isinstance(func, ast.Attribute):
72
+ if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
73
+ attr_name = func.attr
74
+ # Extract signature (remove leading underscore)
75
+ if attr_name.startswith('_'):
76
+ signature_from_decorator = attr_name[1:]
77
+ has_pysealer_decorator = True
78
+ break
79
+
80
+ # Initialize result for this function/class
81
+ result = {
82
+ "has_decorator": has_pysealer_decorator,
83
+ "valid": False,
84
+ "signature": signature_from_decorator,
85
+ "message": "",
86
+ "line_start": node.lineno,
87
+ "line_end": node.end_lineno if hasattr(node, 'end_lineno') and node.end_lineno else node.lineno,
88
+ "source": "",
89
+ "diff": None
90
+ }
91
+
92
+ if not has_pysealer_decorator:
93
+ result["message"] = "No pysealer decorator found"
94
+ results[name] = result
95
+ continue
96
+
97
+ # Extract the source code without pysealer decorators for verification
98
+ node_clone = copy.deepcopy(node)
99
+
100
+ # Filter out pysealer decorators from the clone
101
+ if hasattr(node_clone, 'decorator_list'):
102
+ filtered_decorators = []
103
+
104
+ for decorator in node_clone.decorator_list:
105
+ should_keep = True
106
+
107
+ # Check if decorator is a simple Name node starting with "pysealer"
108
+ if isinstance(decorator, ast.Name):
109
+ if decorator.id.startswith("pysealer"):
110
+ should_keep = False
111
+
112
+ # Check if decorator is an Attribute node (e.g., pysealer.something)
113
+ elif isinstance(decorator, ast.Attribute):
114
+ if isinstance(decorator.value, ast.Name) and decorator.value.id == "pysealer":
115
+ should_keep = False
116
+
117
+ # Check if decorator is a Call node
118
+ elif isinstance(decorator, ast.Call):
119
+ func = decorator.func
120
+ # Check if call is to pysealer.something()
121
+ if isinstance(func, ast.Attribute):
122
+ if isinstance(func.value, ast.Name) and func.value.id == "pysealer":
123
+ should_keep = False
124
+ # Check if call is to pysealer_something()
125
+ elif isinstance(func, ast.Name) and func.id.startswith("pysealer"):
126
+ should_keep = False
127
+
128
+ if should_keep:
129
+ filtered_decorators.append(decorator)
130
+
131
+ node_clone.decorator_list = filtered_decorators
132
+
133
+ # Convert the filtered node back to source code
134
+ module_wrapper = ast.Module(body=[node_clone], type_ignores=[])
135
+ function_source = ast.unparse(module_wrapper)
136
+
137
+ # Store the source code
138
+ result["source"] = function_source
139
+
140
+ # Verify the signature
141
+ try:
142
+ is_valid = verify_signature(function_source, signature_from_decorator, public_key)
143
+
144
+ result["valid"] = is_valid
145
+ if is_valid:
146
+ result["message"] = "✓ Signature valid - code has not been tampered with"
147
+ else:
148
+ result["message"] = "✗ Signature invalid - code may have been modified"
149
+
150
+ # Try to get git diff for failed validation
151
+ try:
152
+ diff = get_function_diff(
153
+ file_path,
154
+ name,
155
+ function_source,
156
+ node.lineno
157
+ )
158
+ if diff:
159
+ result["diff"] = diff
160
+ except Exception:
161
+ # If git diff fails, just continue without it
162
+ pass
163
+
164
+ except Exception as e:
165
+ result["message"] = f"✗ Error verifying signature: {e}"
166
+
167
+ results[name] = result
168
+
169
+ return results
170
+
171
+
172
+ def check_decorators_in_folder(folder_path: str) -> Dict[str, Dict[str, dict]]:
173
+ """
174
+ Check decorators in all Python files in a folder.
175
+
176
+ Args:
177
+ folder_path: Path to the folder containing Python files
178
+
179
+ Returns:
180
+ Dictionary mapping file paths to their verification results
181
+ """
182
+ folder = Path(folder_path)
183
+
184
+ if not folder.exists():
185
+ raise FileNotFoundError(f"Folder '{folder_path}' does not exist.")
186
+
187
+ if not folder.is_dir():
188
+ raise NotADirectoryError(f"'{folder_path}' is not a directory.")
189
+
190
+ # Find all Python files in the folder (non-recursive)
191
+ python_files = list(folder.glob('*.py'))
192
+
193
+ if not python_files:
194
+ raise ValueError(f"No Python files found in '{folder_path}'.")
195
+
196
+ all_results = {}
197
+
198
+ for py_file in python_files:
199
+ try:
200
+ results = check_decorators(str(py_file))
201
+ all_results[str(py_file)] = results
202
+ except Exception as e:
203
+ all_results[str(py_file)] = {"error": str(e)}
204
+
205
+ return all_results