-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprivacy_core.py
More file actions
146 lines (118 loc) · 4.46 KB
/
Copy pathprivacy_core.py
File metadata and controls
146 lines (118 loc) · 4.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
"""Privacy primitives for NullifyPDF export workflows.
This module is deliberately independent from PySide / PyMuPDF so that the
security-sensitive policy and restore-map logic can be tested without a GUI.
"""
from __future__ import annotations
import base64
import json
import os
import re
from dataclasses import asdict, dataclass
from enum import Enum
from typing import Dict, Iterable, List, Optional
class PrivacyMode(str, Enum):
"""Supported privacy export modes."""
ANONYMIZE = "anonymize"
PSEUDONYMIZE = "pseudonymize"
@dataclass(frozen=True)
class PlaceholderEntry:
"""One reversible placeholder mapping entry."""
placeholder: str
original: str
entity_type: str
page: int
class PlaceholderRegistry:
"""Create stable placeholders for detected personal data."""
def __init__(self) -> None:
self._counters: Dict[str, int] = {}
self._by_value: Dict[tuple[str, str], str] = {}
self._entries: List[PlaceholderEntry] = []
@staticmethod
def normalize_entity_type(entity_type: Optional[str]) -> str:
value = (entity_type or "DATA").upper()
value = re.sub(r"[^A-Z0-9_]+", "_", value).strip("_")
return value or "DATA"
def placeholder_for(
self, original: str, entity_type: Optional[str] = None, page: int = 0
) -> str:
clean_original = " ".join((original or "").split())
clean_type = self.normalize_entity_type(entity_type)
key = (clean_type, clean_original.casefold())
if key in self._by_value:
return self._by_value[key]
next_index = self._counters.get(clean_type, 0) + 1
self._counters[clean_type] = next_index
placeholder = f"{clean_type}_{next_index:03d}"
self._by_value[key] = placeholder
self._entries.append(
PlaceholderEntry(
placeholder=placeholder,
original=clean_original,
entity_type=clean_type,
page=max(0, int(page)),
)
)
return placeholder
def entries(self) -> List[PlaceholderEntry]:
return list(self._entries)
def build_restore_payload(
*,
source_name: str,
source_sha256: str,
output_sha256: Optional[str],
entries: Iterable[PlaceholderEntry],
) -> Dict[str, object]:
"""Build the JSON-serializable restore-map payload."""
return {
"format": "NullifyPDF restore map",
"version": 1,
"source_name": os.path.basename(source_name),
"source_sha256": source_sha256,
"output_sha256": output_sha256,
"entries": [asdict(entry) for entry in entries],
}
def encrypt_restore_payload(payload: Dict[str, object], password: str) -> bytes:
"""Encrypt and authenticate a restore-map payload with a password."""
if not password or len(password) < 12:
raise ValueError("La password della mappa deve avere almeno 12 caratteri.")
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
salt = os.urandom(16)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=600000,
)
key = base64.urlsafe_b64encode(kdf.derive(password.encode("utf-8")))
token = Fernet(key).encrypt(
json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8")
)
envelope = {
"format": "NullifyPDF encrypted restore map",
"version": 1,
"kdf": "PBKDF2-HMAC-SHA256",
"iterations": 600000,
"salt": base64.b64encode(salt).decode("ascii"),
"token": token.decode("ascii"),
}
return json.dumps(envelope, ensure_ascii=False, sort_keys=True, indent=2).encode(
"utf-8"
)
def decrypt_restore_payload(data: bytes, password: str) -> Dict[str, object]:
"""Decrypt an encrypted restore map."""
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
envelope = json.loads(data.decode("utf-8"))
salt = base64.b64decode(envelope["salt"])
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=int(envelope["iterations"]),
)
key = base64.urlsafe_b64encode(kdf.derive(password.encode("utf-8")))
plaintext = Fernet(key).decrypt(envelope["token"].encode("ascii"))
return json.loads(plaintext.decode("utf-8"))