django-nativemojo 0.1.10__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.
Files changed (194) hide show
  1. django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
  2. django_nativemojo-0.1.10.dist-info/METADATA +96 -0
  3. django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
  4. django_nativemojo-0.1.10.dist-info/RECORD +194 -0
  5. django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
  6. mojo/__init__.py +3 -0
  7. mojo/apps/account/__init__.py +1 -0
  8. mojo/apps/account/admin.py +91 -0
  9. mojo/apps/account/apps.py +16 -0
  10. mojo/apps/account/migrations/0001_initial.py +77 -0
  11. mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
  12. mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
  13. mojo/apps/account/migrations/__init__.py +0 -0
  14. mojo/apps/account/models/__init__.py +3 -0
  15. mojo/apps/account/models/group.py +98 -0
  16. mojo/apps/account/models/member.py +95 -0
  17. mojo/apps/account/models/pkey.py +18 -0
  18. mojo/apps/account/models/user.py +211 -0
  19. mojo/apps/account/rest/__init__.py +3 -0
  20. mojo/apps/account/rest/group.py +25 -0
  21. mojo/apps/account/rest/user.py +47 -0
  22. mojo/apps/account/utils/__init__.py +0 -0
  23. mojo/apps/account/utils/jwtoken.py +72 -0
  24. mojo/apps/account/utils/passkeys.py +54 -0
  25. mojo/apps/fileman/README.md +549 -0
  26. mojo/apps/fileman/__init__.py +0 -0
  27. mojo/apps/fileman/apps.py +15 -0
  28. mojo/apps/fileman/backends/__init__.py +117 -0
  29. mojo/apps/fileman/backends/base.py +319 -0
  30. mojo/apps/fileman/backends/filesystem.py +397 -0
  31. mojo/apps/fileman/backends/s3.py +398 -0
  32. mojo/apps/fileman/examples/configurations.py +378 -0
  33. mojo/apps/fileman/examples/usage_example.py +665 -0
  34. mojo/apps/fileman/management/__init__.py +1 -0
  35. mojo/apps/fileman/management/commands/__init__.py +1 -0
  36. mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
  37. mojo/apps/fileman/models/__init__.py +7 -0
  38. mojo/apps/fileman/models/file.py +292 -0
  39. mojo/apps/fileman/models/manager.py +227 -0
  40. mojo/apps/fileman/models/render.py +0 -0
  41. mojo/apps/fileman/rest/__init__ +0 -0
  42. mojo/apps/fileman/rest/__init__.py +23 -0
  43. mojo/apps/fileman/rest/fileman.py +13 -0
  44. mojo/apps/fileman/rest/upload.py +92 -0
  45. mojo/apps/fileman/utils/__init__.py +19 -0
  46. mojo/apps/fileman/utils/upload.py +616 -0
  47. mojo/apps/incident/__init__.py +1 -0
  48. mojo/apps/incident/handlers/__init__.py +3 -0
  49. mojo/apps/incident/handlers/event_handlers.py +142 -0
  50. mojo/apps/incident/migrations/0001_initial.py +83 -0
  51. mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
  52. mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
  53. mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
  54. mojo/apps/incident/migrations/__init__.py +0 -0
  55. mojo/apps/incident/models/__init__.py +3 -0
  56. mojo/apps/incident/models/event.py +135 -0
  57. mojo/apps/incident/models/incident.py +33 -0
  58. mojo/apps/incident/models/rule.py +247 -0
  59. mojo/apps/incident/parsers/__init__.py +0 -0
  60. mojo/apps/incident/parsers/ossec/__init__.py +1 -0
  61. mojo/apps/incident/parsers/ossec/core.py +82 -0
  62. mojo/apps/incident/parsers/ossec/parsed.py +23 -0
  63. mojo/apps/incident/parsers/ossec/rules.py +124 -0
  64. mojo/apps/incident/parsers/ossec/utils.py +169 -0
  65. mojo/apps/incident/reporter.py +42 -0
  66. mojo/apps/incident/rest/__init__.py +2 -0
  67. mojo/apps/incident/rest/event.py +23 -0
  68. mojo/apps/incident/rest/ossec.py +22 -0
  69. mojo/apps/logit/__init__.py +0 -0
  70. mojo/apps/logit/admin.py +37 -0
  71. mojo/apps/logit/migrations/0001_initial.py +32 -0
  72. mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
  73. mojo/apps/logit/migrations/0003_log_level.py +18 -0
  74. mojo/apps/logit/migrations/__init__.py +0 -0
  75. mojo/apps/logit/models/__init__.py +1 -0
  76. mojo/apps/logit/models/log.py +57 -0
  77. mojo/apps/logit/rest.py +9 -0
  78. mojo/apps/metrics/README.md +79 -0
  79. mojo/apps/metrics/__init__.py +12 -0
  80. mojo/apps/metrics/redis_metrics.py +331 -0
  81. mojo/apps/metrics/rest/__init__.py +1 -0
  82. mojo/apps/metrics/rest/base.py +152 -0
  83. mojo/apps/metrics/rest/db.py +0 -0
  84. mojo/apps/metrics/utils.py +227 -0
  85. mojo/apps/notify/README.md +91 -0
  86. mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
  87. mojo/apps/notify/__init__.py +0 -0
  88. mojo/apps/notify/admin.py +52 -0
  89. mojo/apps/notify/handlers/__init__.py +0 -0
  90. mojo/apps/notify/handlers/example_handlers.py +516 -0
  91. mojo/apps/notify/handlers/ses/__init__.py +25 -0
  92. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  93. mojo/apps/notify/handlers/ses/complaint.py +25 -0
  94. mojo/apps/notify/handlers/ses/message.py +86 -0
  95. mojo/apps/notify/management/__init__.py +0 -0
  96. mojo/apps/notify/management/commands/__init__.py +1 -0
  97. mojo/apps/notify/management/commands/process_notifications.py +370 -0
  98. mojo/apps/notify/mod +0 -0
  99. mojo/apps/notify/models/__init__.py +12 -0
  100. mojo/apps/notify/models/account.py +128 -0
  101. mojo/apps/notify/models/attachment.py +24 -0
  102. mojo/apps/notify/models/bounce.py +68 -0
  103. mojo/apps/notify/models/complaint.py +40 -0
  104. mojo/apps/notify/models/inbox.py +113 -0
  105. mojo/apps/notify/models/inbox_message.py +173 -0
  106. mojo/apps/notify/models/outbox.py +129 -0
  107. mojo/apps/notify/models/outbox_message.py +288 -0
  108. mojo/apps/notify/models/template.py +30 -0
  109. mojo/apps/notify/providers/__init__.py +0 -0
  110. mojo/apps/notify/providers/aws.py +73 -0
  111. mojo/apps/notify/rest/__init__.py +0 -0
  112. mojo/apps/notify/rest/ses.py +0 -0
  113. mojo/apps/notify/utils/__init__.py +2 -0
  114. mojo/apps/notify/utils/notifications.py +404 -0
  115. mojo/apps/notify/utils/parsing.py +202 -0
  116. mojo/apps/notify/utils/render.py +144 -0
  117. mojo/apps/tasks/README.md +118 -0
  118. mojo/apps/tasks/__init__.py +11 -0
  119. mojo/apps/tasks/manager.py +489 -0
  120. mojo/apps/tasks/rest/__init__.py +2 -0
  121. mojo/apps/tasks/rest/hooks.py +0 -0
  122. mojo/apps/tasks/rest/tasks.py +62 -0
  123. mojo/apps/tasks/runner.py +174 -0
  124. mojo/apps/tasks/tq_handlers.py +14 -0
  125. mojo/decorators/__init__.py +3 -0
  126. mojo/decorators/auth.py +25 -0
  127. mojo/decorators/cron.py +31 -0
  128. mojo/decorators/http.py +132 -0
  129. mojo/decorators/validate.py +14 -0
  130. mojo/errors.py +88 -0
  131. mojo/helpers/__init__.py +0 -0
  132. mojo/helpers/aws/__init__.py +0 -0
  133. mojo/helpers/aws/client.py +8 -0
  134. mojo/helpers/aws/s3.py +268 -0
  135. mojo/helpers/aws/setup_email.py +0 -0
  136. mojo/helpers/cron.py +79 -0
  137. mojo/helpers/crypto/__init__.py +4 -0
  138. mojo/helpers/crypto/aes.py +60 -0
  139. mojo/helpers/crypto/hash.py +59 -0
  140. mojo/helpers/crypto/privpub/__init__.py +1 -0
  141. mojo/helpers/crypto/privpub/hybrid.py +97 -0
  142. mojo/helpers/crypto/privpub/rsa.py +104 -0
  143. mojo/helpers/crypto/sign.py +36 -0
  144. mojo/helpers/crypto/too.l.py +25 -0
  145. mojo/helpers/crypto/utils.py +26 -0
  146. mojo/helpers/daemon.py +94 -0
  147. mojo/helpers/dates.py +69 -0
  148. mojo/helpers/dns/__init__.py +0 -0
  149. mojo/helpers/dns/godaddy.py +62 -0
  150. mojo/helpers/filetypes.py +128 -0
  151. mojo/helpers/logit.py +310 -0
  152. mojo/helpers/modules.py +95 -0
  153. mojo/helpers/paths.py +63 -0
  154. mojo/helpers/redis.py +10 -0
  155. mojo/helpers/request.py +89 -0
  156. mojo/helpers/request_parser.py +269 -0
  157. mojo/helpers/response.py +14 -0
  158. mojo/helpers/settings.py +146 -0
  159. mojo/helpers/sysinfo.py +140 -0
  160. mojo/helpers/ua.py +0 -0
  161. mojo/middleware/__init__.py +0 -0
  162. mojo/middleware/auth.py +26 -0
  163. mojo/middleware/logging.py +55 -0
  164. mojo/middleware/mojo.py +21 -0
  165. mojo/migrations/0001_initial.py +32 -0
  166. mojo/migrations/__init__.py +0 -0
  167. mojo/models/__init__.py +2 -0
  168. mojo/models/meta.py +262 -0
  169. mojo/models/rest.py +538 -0
  170. mojo/models/secrets.py +59 -0
  171. mojo/rest/__init__.py +1 -0
  172. mojo/rest/info.py +26 -0
  173. mojo/serializers/__init__.py +0 -0
  174. mojo/serializers/models.py +165 -0
  175. mojo/serializers/openapi.py +188 -0
  176. mojo/urls.py +38 -0
  177. mojo/ws4redis/README.md +174 -0
  178. mojo/ws4redis/__init__.py +2 -0
  179. mojo/ws4redis/client.py +283 -0
  180. mojo/ws4redis/connection.py +327 -0
  181. mojo/ws4redis/exceptions.py +32 -0
  182. mojo/ws4redis/redis.py +183 -0
  183. mojo/ws4redis/servers/__init__.py +0 -0
  184. mojo/ws4redis/servers/base.py +86 -0
  185. mojo/ws4redis/servers/django.py +171 -0
  186. mojo/ws4redis/servers/uwsgi.py +63 -0
  187. mojo/ws4redis/settings.py +45 -0
  188. mojo/ws4redis/utf8validator.py +128 -0
  189. mojo/ws4redis/websocket.py +403 -0
  190. testit/__init__.py +0 -0
  191. testit/client.py +147 -0
  192. testit/faker.py +20 -0
  193. testit/helpers.py +198 -0
  194. testit/runner.py +262 -0
mojo/helpers/aws/s3.py ADDED
@@ -0,0 +1,268 @@
1
+ from rest import settings
2
+ from objict import objict
3
+ import boto3
4
+ import botocore
5
+ from urllib.parse import urlparse
6
+ import io
7
+ import sys
8
+ from medialib import utils
9
+ import threading
10
+ import tempfile
11
+ import logging
12
+ from typing import Optional, Union, BinaryIO, Dict, List, Any
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ class S3Config:
17
+ """S3 configuration holder with lazy initialization of clients and resources."""
18
+ def __init__(self, key: str, secret: str):
19
+ self.key = key
20
+ self.secret = secret
21
+ self._resource = None
22
+ self._client = None
23
+
24
+ @property
25
+ def resource(self):
26
+ if self._resource is None:
27
+ self._resource = boto3.resource('s3',
28
+ aws_access_key_id=self.key,
29
+ aws_secret_access_key=self.secret)
30
+ return self._resource
31
+
32
+ @property
33
+ def client(self):
34
+ if self._client is None:
35
+ self._client = boto3.client('s3',
36
+ aws_access_key_id=self.key,
37
+ aws_secret_access_key=self.secret)
38
+ return self._client
39
+
40
+ # Initialize the global S3 configuration
41
+ S3 = S3Config(settings.AWS_KEY, settings.AWS_SECRET)
42
+
43
+
44
+ class S3Item:
45
+ """Class representing an S3 object with operations for upload, download, and management."""
46
+
47
+ S3_HOST = "https://s3.amazonaws.com"
48
+
49
+ def __init__(self, url: str):
50
+ """
51
+ Initialize an S3Item from a URL.
52
+
53
+ Args:
54
+ url: S3 URL in the format s3://bucket-name/key
55
+ """
56
+ self.url = url
57
+ parsed_url = urlparse(url)
58
+ self.bucket_name = parsed_url.netloc
59
+ self.key = parsed_url.path.lstrip('/')
60
+ self.object = S3.resource.Object(self.bucket_name, self.key)
61
+ self.exists = self._check_exists()
62
+
63
+ def _check_exists(self) -> bool:
64
+ """Check if the S3 object exists."""
65
+ try:
66
+ self.object.load()
67
+ return True
68
+ except botocore.exceptions.ClientError as e:
69
+ if e.response['Error']['Code'] == "404":
70
+ return False
71
+ raise
72
+
73
+ def upload(self, file_obj: Union[str, BinaryIO], background: bool = False) -> None:
74
+ """
75
+ Upload a file to S3.
76
+
77
+ Args:
78
+ file_obj: File path or file-like object to upload
79
+ background: Currently unused, kept for backward compatibility
80
+ """
81
+ prepared_file = self._prepare_file(file_obj)
82
+ self.object.upload_fileobj(prepared_file)
83
+
84
+ def create_multipart_upload(self) -> str:
85
+ """
86
+ Initialize a multipart upload.
87
+
88
+ Returns:
89
+ Upload ID for the multipart upload
90
+ """
91
+ self.part_num = 0
92
+ self.parts = []
93
+ response = S3.client.create_multipart_upload(
94
+ Bucket=self.bucket_name,
95
+ Key=self.key
96
+ )
97
+ self.upload_id = response["UploadId"]
98
+ return self.upload_id
99
+
100
+ def upload_part(self, chunk: bytes) -> Dict:
101
+ """
102
+ Upload a part in a multipart upload.
103
+
104
+ Args:
105
+ chunk: Bytes to upload as part
106
+
107
+ Returns:
108
+ Dict with part information
109
+ """
110
+ self.part_num += 1
111
+ response = S3.client.upload_part(
112
+ Bucket=self.bucket_name,
113
+ Key=self.key,
114
+ PartNumber=self.part_num,
115
+ UploadId=self.upload_id,
116
+ Body=chunk
117
+ )
118
+ part_info = {"PartNumber": self.part_num, "ETag": response["ETag"]}
119
+ self.parts.append(part_info)
120
+ return part_info
121
+
122
+ def complete_multipart_upload(self) -> Dict:
123
+ """
124
+ Complete a multipart upload.
125
+
126
+ Returns:
127
+ S3 response
128
+ """
129
+ return S3.client.complete_multipart_upload(
130
+ Bucket=self.bucket_name,
131
+ Key=self.key,
132
+ UploadId=self.upload_id,
133
+ MultipartUpload={"Parts": self.parts}
134
+ )
135
+
136
+ @property
137
+ def public_url(self) -> str:
138
+ """Get the public URL for the S3 object."""
139
+ return f"{self.S3_HOST}/{self.bucket_name}/{self.key}"
140
+
141
+ def generate_presigned_url(self, expires: int = 600) -> str:
142
+ """
143
+ Generate a presigned URL for the S3 object.
144
+
145
+ Args:
146
+ expires: Expiration time in seconds
147
+
148
+ Returns:
149
+ Presigned URL
150
+ """
151
+ return S3.client.generate_presigned_url(
152
+ 'get_object',
153
+ ExpiresIn=expires,
154
+ Params={'Bucket': self.bucket_name, 'Key': self.key}
155
+ )
156
+
157
+ def download(self, file_obj: Optional[BinaryIO] = None) -> BinaryIO:
158
+ """
159
+ Download the S3 object.
160
+
161
+ Args:
162
+ file_obj: Optional file-like object to download to
163
+
164
+ Returns:
165
+ File-like object containing the downloaded data
166
+ """
167
+ if file_obj is None:
168
+ file_obj = tempfile.NamedTemporaryFile()
169
+ self.object.download_fileobj(file_obj)
170
+ if hasattr(file_obj, 'seek'):
171
+ file_obj.seek(0)
172
+ return file_obj
173
+
174
+ def delete(self) -> None:
175
+ """Delete the S3 object."""
176
+ self.object.delete()
177
+
178
+ def _prepare_file(self, file_obj: Union[str, BinaryIO]) -> BinaryIO:
179
+ """
180
+ Prepare a file object for upload.
181
+
182
+ Args:
183
+ file_obj: File path or file-like object
184
+
185
+ Returns:
186
+ A file-like object ready for upload
187
+ """
188
+ if hasattr(file_obj, "read"):
189
+ return io.BytesIO(file_obj.read().encode() if isinstance(file_obj.read(), str) else file_obj.read())
190
+
191
+ try:
192
+ return open(str(file_obj), "rb")
193
+ except (IOError, TypeError):
194
+ pass
195
+
196
+ return file_obj
197
+
198
+
199
+
200
+ # Utility functions for common S3 operations
201
+
202
+ def upload(url: str, file_obj: Union[str, BinaryIO], background: bool = False) -> None:
203
+ """Upload a file to S3."""
204
+ S3Item(url).upload(file_obj, background)
205
+
206
+
207
+ def view_url_noexpire(url: str, is_secure: bool = False) -> str:
208
+ """Get a public URL for an S3 object."""
209
+ return S3Item(url).public_url
210
+
211
+
212
+ def view_url(url: str, expires: Optional[int] = 600, is_secure: bool = True) -> str:
213
+ """
214
+ Get a URL for an S3 object.
215
+
216
+ Args:
217
+ url: S3 URL
218
+ expires: Expiration time in seconds, or None for a public URL
219
+ is_secure: Whether to use HTTPS (currently unused)
220
+
221
+ Returns:
222
+ URL for the S3 object
223
+ """
224
+ if expires is None:
225
+ return view_url_noexpire(url, is_secure)
226
+ return S3Item(url).generate_presigned_url(expires)
227
+
228
+
229
+ def exists(url: str) -> bool:
230
+ """Check if an S3 object exists."""
231
+ return S3Item(url).exists
232
+
233
+
234
+ def get_file(url: str, file_obj: Optional[BinaryIO] = None) -> BinaryIO:
235
+ """Download an S3 object to a file."""
236
+ return S3Item(url).download(file_obj)
237
+
238
+
239
+ def delete(url: str) -> None:
240
+ """
241
+ Delete an S3 object or prefix.
242
+
243
+ If the URL ends with /, all objects under that prefix are deleted.
244
+ """
245
+ if url.endswith("/"):
246
+ parsed_url = urlparse(url)
247
+ prefix = parsed_url.path.lstrip("/")
248
+ bucket_name = parsed_url.netloc
249
+
250
+ response = S3.client.list_objects_v2(
251
+ Bucket=bucket_name,
252
+ Prefix=prefix,
253
+ MaxKeys=100
254
+ )
255
+
256
+ if 'Contents' in response:
257
+ for obj in response['Contents']:
258
+ S3.client.delete_object(
259
+ Bucket=bucket_name,
260
+ Key=obj['Key']
261
+ )
262
+ else:
263
+ S3Item(url).delete()
264
+
265
+
266
+ def path(url: str) -> str:
267
+ """Extract the path component from a URL."""
268
+ return urlparse(url).path
File without changes
mojo/helpers/cron.py ADDED
@@ -0,0 +1,79 @@
1
+ import datetime
2
+ from typing import Callable, List, Dict, Any
3
+ from mojo.decorators.cron import schedule
4
+
5
+ def run_now() -> None:
6
+ """
7
+ Execute the scheduled functions that match the current time.
8
+
9
+ Retrieves the list of functions scheduled to run at the current
10
+ date and time, and executes each of them.
11
+ """
12
+ functions_to_run = find_scheduled_functions()
13
+ for func in functions_to_run:
14
+ func()
15
+
16
+ def find_scheduled_functions() -> List[Callable]:
17
+ """
18
+ Find all functions that are scheduled to run at the current time.
19
+
20
+ Returns:
21
+ List[Callable]: A list of callable functions that match the
22
+ current date and time according to their cron specifications.
23
+ """
24
+ if not hasattr(schedule, 'scheduled_functions'):
25
+ return []
26
+
27
+ now = datetime.datetime.now()
28
+ matching_funcs = []
29
+
30
+ for cron_spec in schedule.scheduled_functions:
31
+ if match_time(now, cron_spec):
32
+ matching_funcs.append(cron_spec['func'])
33
+
34
+ return matching_funcs
35
+
36
+ def match_time(current_time: datetime.datetime, cron_spec: Dict[str, Any]) -> bool:
37
+ """
38
+ Determine if a given time matches a cron-like specification.
39
+
40
+ Args:
41
+ current_time (datetime.datetime): The current date and time.
42
+ cron_spec (Dict[str, Any]): A dictionary containing the cron
43
+ specifications to match against.
44
+
45
+ Returns:
46
+ bool: True if the current time matches the cron specification,
47
+ False otherwise.
48
+ """
49
+ cron_field = {
50
+ 'minutes': current_time.minute,
51
+ 'hours': current_time.hour,
52
+ 'days': current_time.day,
53
+ 'months': current_time.month,
54
+ 'weekdays': current_time.weekday()
55
+ }
56
+
57
+ for field, time_value in cron_field.items():
58
+ if not matches(cron_spec[field], time_value):
59
+ return False
60
+ return True
61
+
62
+ def matches(cron_value: str, actual_value: int) -> bool:
63
+ """
64
+ Check if a specific time value matches the corresponding cron pattern.
65
+
66
+ Args:
67
+ cron_value (str): The cron pattern to match, which can be '*'
68
+ or a list of comma-separated values.
69
+ actual_value (int): The actual time value to compare.
70
+
71
+ Returns:
72
+ bool: True if the actual value matches the cron pattern,
73
+ False otherwise.
74
+ """
75
+ if cron_value == '*':
76
+ return True
77
+ cron_values = cron_value.split(',')
78
+
79
+ return str(actual_value) in cron_values
@@ -0,0 +1,4 @@
1
+ from .aes import encrypt, decrypt
2
+ from .utils import random_bytes, random_string
3
+ from .hash import hash
4
+ from .sign import generate_signature as sign, verify_signature as verify
@@ -0,0 +1,60 @@
1
+ import json
2
+ from base64 import b64encode, b64decode
3
+ from Crypto.Cipher import AES
4
+ from Crypto.Protocol.KDF import PBKDF2
5
+ from Crypto.Random import get_random_bytes
6
+ from objict import objict
7
+ import mojo.errors
8
+
9
+ PBKDF2_ITERATIONS = 100_000
10
+ SALT_LENGTH = 16
11
+ NONCE_LENGTH = 12
12
+ TAG_LENGTH = 16
13
+
14
+
15
+ def encrypt(data, password):
16
+ if isinstance(data, dict):
17
+ data = json.dumps(data)
18
+ if not isinstance(data, str):
19
+ raise mojo.errors.ValueException("Data must be a string or dictionary")
20
+
21
+ data_bytes = data.encode('utf-8')
22
+ salt = get_random_bytes(SALT_LENGTH)
23
+ key = derive_key(password, salt)
24
+ cipher = AES.new(key, AES.MODE_GCM, nonce=get_random_bytes(NONCE_LENGTH))
25
+
26
+ ciphertext, tag = cipher.encrypt_and_digest(data_bytes)
27
+
28
+ # Final payload: [salt | nonce | tag | ciphertext]
29
+ payload = salt + cipher.nonce + tag + ciphertext
30
+ return b64encode(payload).decode('utf-8')
31
+
32
+ def decrypt(enc_data_b64, password, ignore_errors=True):
33
+ raw = b64decode(enc_data_b64)
34
+
35
+ salt = raw[:SALT_LENGTH]
36
+ nonce = raw[SALT_LENGTH:SALT_LENGTH + NONCE_LENGTH]
37
+ tag = raw[SALT_LENGTH + NONCE_LENGTH:SALT_LENGTH + NONCE_LENGTH + TAG_LENGTH]
38
+ ciphertext = raw[SALT_LENGTH + NONCE_LENGTH + TAG_LENGTH:]
39
+
40
+ key = derive_key(password, salt)
41
+ cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
42
+
43
+ if ignore_errors:
44
+ try:
45
+ decrypted = cipher.decrypt_and_verify(ciphertext, tag)
46
+ except ValueError:
47
+ return None
48
+ else:
49
+ decrypted = cipher.decrypt_and_verify(ciphertext, tag)
50
+
51
+ decrypted_str = decrypted.decode('utf-8')
52
+
53
+ try:
54
+ return objict.from_json(decrypted_str)
55
+ except Exception:
56
+ return decrypted_str
57
+
58
+
59
+ def derive_key(password, salt, key_length=32):
60
+ return PBKDF2(password, salt, dkLen=key_length, count=PBKDF2_ITERATIONS)
@@ -0,0 +1,59 @@
1
+ from Crypto.Hash import SHA256
2
+ from Crypto.Random import get_random_bytes
3
+ import hmac
4
+ import hashlib
5
+ from django.conf import settings
6
+
7
+
8
+ def hash(value, salt=settings.SECRET_KEY):
9
+ """
10
+ Returns a SHA-256 hash of the input value (string, int, or dict), optionally salted.
11
+
12
+ :param value: str, int, or dict - the input to be hashed
13
+ :param salt: Optional[str or bytes] - a salt to strengthen the hash
14
+ :return: str - the hex digest of the hash
15
+ """
16
+ if isinstance(value, dict):
17
+ # Sort the dictionary and prepare a string representation
18
+ value_str = str(sorted(value.items())).encode('utf-8')
19
+ elif isinstance(value, (str, int)):
20
+ value_str = str(value).encode('utf-8')
21
+ else:
22
+ raise TypeError("Only strings, integers, or dictionaries are allowed.")
23
+
24
+ # Use provided salt or generate one
25
+ if salt is None:
26
+ salt = get_random_bytes(16)
27
+ elif isinstance(salt, str):
28
+ salt = salt.encode('utf-8')
29
+
30
+ # Combine salt and value
31
+ hasher = hashlib.sha256()
32
+ hasher.update(salt + value_str)
33
+ return hasher.hexdigest()
34
+
35
+
36
+ def hash_digits(digits, secret_key):
37
+ """Hashes the digits using a derived salt without storing it."""
38
+ salt = derive_salt(digits, secret_key)
39
+ hash_obj = hashlib.sha256(salt + digits.encode())
40
+ return hash_obj.hexdigest()
41
+
42
+
43
+ def derive_salt(digits, secret_key):
44
+ """Derives a salt from the last 8 digits of the DIGITs using HMAC."""
45
+ last_8_digits = digits[-8:]
46
+ if isinstance(secret_key, str): # Ensure secret_key is bytes
47
+ secret_key = secret_key.encode()
48
+ return hmac.new(secret_key, last_8_digits.encode(), hashlib.sha256).digest()[:16] # Use first 16 bytes
49
+
50
+
51
+ def hash_to_hex(input_string):
52
+ if not isinstance(input_string, str):
53
+ raise ValueError("Input must be a string")
54
+ # Create a new SHA-256 hasher
55
+ hasher = hashlib.sha256()
56
+ # Update the hasher with the input string encoded to bytes
57
+ hasher.update(input_string.encode('utf-8'))
58
+ # Return the hexadecimal representation of the hash
59
+ return hasher.hexdigest()
@@ -0,0 +1 @@
1
+ from .hybrid import PrivatePublicEncryption
@@ -0,0 +1,97 @@
1
+ import base64
2
+ import binascii
3
+ import json
4
+ from nacl.public import PrivateKey, PublicKey, SealedBox
5
+ from nacl.encoding import Base64Encoder
6
+ from nacl.exceptions import CryptoError
7
+
8
+
9
+ class PrivatePublicEncryption:
10
+ def __init__(self, private_key=None, public_key=None, private_key_file=None, public_key_file=None):
11
+ self.private_key = self._load_key(private_key, private_key_file, is_private=True)
12
+ self.public_key = self._load_key(public_key, public_key_file, is_private=False)
13
+
14
+ if self.private_key and not self.public_key:
15
+ self.public_key = self.private_key.public_key
16
+
17
+ def _load_key(self, key, key_file, is_private):
18
+ if key_file:
19
+ with open(key_file, 'r') as f:
20
+ key = f.read().strip()
21
+
22
+ if key:
23
+ if isinstance(key, str):
24
+ key_bytes = Base64Encoder.decode(key)
25
+ elif isinstance(key, bytes):
26
+ key_bytes = key
27
+ else:
28
+ raise ValueError("Key must be a base64 string or bytes")
29
+
30
+ return PrivateKey(key_bytes) if is_private else PublicKey(key_bytes)
31
+ return None
32
+
33
+ def generate_public_key(self, make_new=False):
34
+ if self.public_key is None or make_new:
35
+ if not self.private_key:
36
+ self.private_key = PrivateKey.generate()
37
+ self.public_key = self.private_key.public_key
38
+ return self.public_key
39
+
40
+ def encrypt(self, data):
41
+ self.generate_public_key()
42
+ return self.encrypt_to_b64(data)
43
+
44
+ def decrypt(self, data, as_string=True):
45
+ return self.decrypt_from_b64(data, as_string)
46
+
47
+ def encrypt_to_b64(self, data):
48
+ encrypted_bytes = encrypt_with_public_key(data, self.public_key)
49
+ return base64.b64encode(encrypted_bytes).decode('utf-8')
50
+
51
+ def decrypt_from_b64(self, data, as_string=True):
52
+ decoded = base64.b64decode(data)
53
+ return decrypt_with_private_key(decoded, self.private_key, as_string)
54
+
55
+ def encrypt_to_hex(self, data):
56
+ encrypted_bytes = encrypt_with_public_key(data, self.public_key)
57
+ return binascii.hexlify(encrypted_bytes).decode('utf-8')
58
+
59
+ def decrypt_from_hex(self, data, as_string=True):
60
+ decoded = binascii.unhexlify(data)
61
+ return decrypt_with_private_key(decoded, self.private_key, as_string)
62
+
63
+
64
+ def generate_private_key():
65
+ return PrivateKey.generate()
66
+
67
+
68
+ def generate_public_key(private_key):
69
+ if isinstance(private_key, str):
70
+ private_key = PrivateKey(Base64Encoder.decode(private_key))
71
+ return private_key.public_key
72
+
73
+
74
+ def encrypt_with_public_key(data, public_key):
75
+ if isinstance(public_key, str):
76
+ public_key = PublicKey(Base64Encoder.decode(public_key))
77
+
78
+ if isinstance(data, (dict, list)):
79
+ data = json.dumps(data)
80
+ if isinstance(data, str):
81
+ data = data.encode('utf-8')
82
+
83
+ sealed_box = SealedBox(public_key)
84
+ return sealed_box.encrypt(data)
85
+
86
+
87
+ def decrypt_with_private_key(data, private_key, as_string=True):
88
+ if isinstance(private_key, str):
89
+ private_key = PrivateKey(Base64Encoder.decode(private_key))
90
+
91
+ sealed_box = SealedBox(private_key)
92
+ try:
93
+ decrypted = sealed_box.decrypt(data)
94
+ decoded = decrypted.decode('utf-8')
95
+ return json.loads(decoded) if as_string else decrypted
96
+ except (CryptoError, json.JSONDecodeError):
97
+ return decrypted.decode('utf-8') if as_string else decrypted
@@ -0,0 +1,104 @@
1
+ from Crypto import Random
2
+ from Crypto.PublicKey import RSA
3
+ from Crypto.Cipher import AES, PKCS1_OAEP
4
+ import base64
5
+ import binascii
6
+ import json
7
+ from io import BytesIO
8
+ from contextlib import closing
9
+
10
+
11
+ class PrivatePublicEncryption:
12
+ def __init__(self, private_key=None, public_key=None, private_key_file=None, public_key_file=None):
13
+ self.private_key = self._load_key(private_key, private_key_file)
14
+ self.public_key = self._load_key(public_key, public_key_file)
15
+
16
+ def _load_key(self, key, key_file):
17
+ if key_file:
18
+ with open(key_file, 'r') as f:
19
+ key = f.read()
20
+ if isinstance(key, str):
21
+ return RSA.import_key(key)
22
+ return key
23
+
24
+ def generate_public_key(self, make_new=False):
25
+ if self.public_key is None or make_new:
26
+ self.public_key = generate_public_key(self.private_key)
27
+ return self.public_key
28
+
29
+ def encrypt(self, data):
30
+ self.generate_public_key()
31
+ return self.encrypt_to_b64(data)
32
+
33
+ def decrypt(self, data, as_string=True):
34
+ return self.decrypt_from_b64(data, as_string)
35
+
36
+ def encrypt_to_b64(self, data):
37
+ ebytes = encrypt_with_public_key(data, self.public_key)
38
+ return base64.b64encode(ebytes).decode('utf-8')
39
+
40
+ def decrypt_from_b64(self, data, as_string=True):
41
+ data = base64.b64decode(data)
42
+ return decrypt_with_private_key(data, self.private_key, as_string)
43
+
44
+ def encrypt_to_hex(self, data):
45
+ ebytes = encrypt_with_public_key(data, self.public_key)
46
+ return binascii.hexlify(ebytes).decode('utf-8')
47
+
48
+ def decrypt_from_hex(self, data, as_string=True):
49
+ data = binascii.unhexlify(data)
50
+ return decrypt_with_private_key(data, self.private_key, as_string)
51
+
52
+
53
+ def generate_private_key(size=2048):
54
+ return RSA.generate(size)
55
+
56
+
57
+ def generate_public_key(private_key):
58
+ if isinstance(private_key, str):
59
+ private_key = RSA.import_key(private_key)
60
+ return private_key.publickey()
61
+
62
+
63
+ def encrypt_with_public_key(data, public_key):
64
+ if isinstance(public_key, str):
65
+ public_key = RSA.import_key(public_key)
66
+
67
+ if isinstance(data, (dict, list)):
68
+ data = json.dumps(data)
69
+
70
+ if isinstance(data, str):
71
+ data = data.encode('utf-8')
72
+
73
+ session_key = Random.get_random_bytes(16)
74
+ cipher_rsa = PKCS1_OAEP.new(public_key)
75
+ enc_session_key = cipher_rsa.encrypt(session_key)
76
+
77
+ cipher_aes = AES.new(session_key, AES.MODE_EAX)
78
+ ciphertext, tag = cipher_aes.encrypt_and_digest(data)
79
+
80
+ with closing(BytesIO()) as output:
81
+ for x in (enc_session_key, cipher_aes.nonce, tag, ciphertext):
82
+ output.write(x)
83
+ return output.getvalue()
84
+
85
+
86
+ def decrypt_with_private_key(data, private_key, as_string=True):
87
+ if isinstance(private_key, str):
88
+ private_key = RSA.import_key(private_key)
89
+ if isinstance(data, str):
90
+ data = data.encode('utf-8')
91
+ if isinstance(data, bytes):
92
+ data = BytesIO(data)
93
+
94
+ enc_session_key, nonce, tag, ciphertext = (
95
+ data.read(x) for x in (private_key.size_in_bytes(), 16, 16, -1)
96
+ )
97
+
98
+ cipher_rsa = PKCS1_OAEP.new(private_key)
99
+ session_key = cipher_rsa.decrypt(enc_session_key)
100
+
101
+ cipher_aes = AES.new(session_key, AES.MODE_EAX, nonce)
102
+ decrypted_data = cipher_aes.decrypt_and_verify(ciphertext, tag)
103
+
104
+ return decrypted_data.decode('utf-8') if as_string else decrypted_data