artemis_framework 0.1.2__tar.gz → 0.1.4__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: artemis_framework
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A package for doing great things!
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from artemis_framework.asphodel.core.gpg_context import build_gpg, check_result
7
+
8
+
9
+ def encrypt_to_recipients(
10
+ gpg: "gnupg.GPG", # type: ignore
11
+ plaintext: str | bytes,
12
+ recipients: list[str],
13
+ *,
14
+ armor: bool = True,
15
+ sign: str | None = None,
16
+ passphrase: str | None = None,
17
+ always_trust: bool = True,
18
+ extra_args: list[str] | None = None
19
+ ) -> str | bytes | None:
20
+ result = gpg.encrypt(
21
+ plaintext,
22
+ recipients,
23
+ armor=armor,
24
+ sign=sign,
25
+ passphrase=passphrase,
26
+ always_trust=always_trust,
27
+ extra_args=extra_args or []
28
+ )
29
+
30
+ if not check_result(result, label=f"encrypt_to_recipients({recipients})"):
31
+ return None
32
+
33
+ return result.data.decode("ascii") if armor else result.data
34
+
35
+
36
+ def encrypt_symmetric(
37
+ gpg: "gnupg.GPG", # type: ignore
38
+ plaintext: str | bytes,
39
+ passphrase: str,
40
+ *,
41
+ armor: bool = True,
42
+ cipher_algo: str = "AES256"
43
+ ) -> str | bytes | None:
44
+ result = gpg.encrypt(
45
+ plaintext,
46
+ recipients=None,
47
+ symmetric=True,
48
+ passphrase=passphrase,
49
+ armor=armor,
50
+ extra_args=["--cipher-algo", cipher_algo, "--no-symkey-cache"]
51
+ )
52
+
53
+ if not check_result(result, label="encrypt_symmetric"):
54
+ return None
55
+
56
+ return result.data.decode("ascii") if armor else result.data
57
+
58
+
59
+ def decrypt_data(
60
+ gpg: "gnupg.GPG", # type: ignore
61
+ ciphertext: str | bytes,
62
+ *,
63
+ passphrase: str | None = None,
64
+ always_trust: bool = True
65
+ ) -> bytes | None:
66
+ result = gpg.decrypt(
67
+ ciphertext,
68
+ passphrase=passphrase,
69
+ always_trust=always_trust
70
+ )
71
+
72
+ if not check_result(result, label="decrypt_data"):
73
+ return None
74
+
75
+ return result.data
76
+
77
+
78
+ def encrypt_file(
79
+ gpg: "gnupg.GPG", # type: ignore
80
+ input_path: Path | str,
81
+ recipients: list[str],
82
+ *,
83
+ output_path: Path | str | None = None,
84
+ armor: bool = True,
85
+ sign : str | None = None,
86
+ passphrase: str | None = None,
87
+ always_trust: bool = True
88
+ ) -> Path | None:
89
+ src = Path(input_path)
90
+ if not src.exists():
91
+ print(f"[ERR] encrypt_file: {src} not found.")
92
+ return None
93
+
94
+ dst = Path(output_path) if output_path else src.with_suffix(src.suffix + (".asc" if armor else ".gpg"))
95
+
96
+ with open(src, "rb") as file:
97
+ result = gpg.encrypt_file(
98
+ file,
99
+ recipients=recipients,
100
+ armor=armor,
101
+ sign=sign,
102
+ passphrase=passphrase,
103
+ always_trust=always_trust,
104
+ output=str(dst)
105
+ )
106
+
107
+ if not check_result(result, label=f"encrypt_file({src.name})"):
108
+ return None
109
+
110
+ print(f" output: {dst}")
111
+ return dst
112
+
113
+
114
+ def decrypt_file(
115
+ gpg: "gnupg.GPG", # type: ignore
116
+ input_path: Path | str,
117
+ *,
118
+ output_path: Path | str | None = None,
119
+ passphrase: str | None = None,
120
+ always_trust: bool = True
121
+ ) -> Path | None:
122
+ src = Path(input_path)
123
+ if not src.exists():
124
+ print(f"[ERR] decrypt_file: {src} not found.")
125
+ return None
126
+
127
+ if output_path:
128
+ dst = Path(output_path)
129
+ else:
130
+ dst = src.with_suffix("")
131
+
132
+ with open(src, "rb") as file:
133
+ result = gpg.decrypt_file(
134
+ file,
135
+ passphrase=passphrase,
136
+ always_trust=always_trust,
137
+ output=str(dst)
138
+ )
139
+
140
+ if not check_result(result, label=f"decrypt_file({src.name})"):
141
+ return None
142
+
143
+ print(f" output: {dst}")
144
+ return dst
145
+
146
+
147
+ def print_decrypt_signature_info(result) -> None:
148
+ sig_id = getattr(result, "signature_id", None)
149
+ key_id = getattr(result, "key_id", None)
150
+ uid = getattr(result, "username", None)
151
+ trust = getattr(result, "trust_text", None)
152
+
153
+ if sig_id or key_id:
154
+ print("\n [Signature embedded in ciphertext]")
155
+ print(f" signer UID : {uid or '<unknown>'}")
156
+ print(f" key ID : {key_id or '<unknown>'}")
157
+ print(f" trust : {trust or '<unknown>'}")
158
+ else:
159
+ print("\n [No embedded signature detected]")
160
+
@@ -0,0 +1,257 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from artemis_framework.asphodel.core.gpg_context import build_gpg, check_result
8
+
9
+
10
+ @dataclass
11
+ class VerificationResult:
12
+ valid: bool
13
+ fingerprint: str
14
+ key_id: str
15
+ username: str
16
+ status: str
17
+ timestamp: int | None
18
+ expire_timestamp: int | None
19
+ trust_level: str
20
+ stderr: str
21
+
22
+ def __str__(self) -> str:
23
+ lines = (
24
+ f" valid : {self.valid}",
25
+ f" fingerprint : {self.fingerprint}",
26
+ f" key_id : {self.key_id}",
27
+ f" username : {self.username}",
28
+ f" status : {self.status}",
29
+ f" timestamp : {self.timestamp}",
30
+ f" trust_level : {self.trust_level}",
31
+ )
32
+ return "\n".join(lines)
33
+
34
+ @classmethod
35
+ def from_gnupg(cls, result) -> "VerificationResult":
36
+ sigs = getattr(result, "sig_info", {})
37
+
38
+ if sigs:
39
+ fp, info = next(iter(sigs.items()))
40
+ key_id = info.get("keyid", "")
41
+ username = info.get("username", "")
42
+ status = info.get("status", "")
43
+ ts = info.get("timestamp")
44
+ exp_ts = info.get("expire_timestamp")
45
+ trust = info.get("trust_level", "")
46
+ else:
47
+ fp = getattr(result, "fingerprint", "") or ""
48
+ key_id = getattr(result, "key_id", "") or ""
49
+ username = getattr(result, "username", "") or ""
50
+ status = getattr(result, "status", "") or ""
51
+ ts = getattr(result, "timestamp", None)
52
+ exp_ts = None
53
+ trust = getattr(result, "trust_level", "") or ""
54
+ valid = bool(getattr(result, "valid", False))
55
+
56
+ return cls(
57
+ valid=valid,
58
+ fingerprint=fp,
59
+ key_id=key_id,
60
+ username=username,
61
+ status=status,
62
+ timestamp=int(ts) if ts else None,
63
+ expire_timestamp=int(exp_ts) if exp_ts else None,
64
+ trust_level=trust,
65
+ stderr=getattr(result, "stderr", "") or ""
66
+ )
67
+
68
+
69
+ def clearsign(
70
+ gpg,
71
+ message: str | bytes,
72
+ signing_fingerprint: str,
73
+ *,
74
+ passphrase: str | None = None,
75
+ detach: bool = False
76
+ ) -> str | None:
77
+ result = gpg.sign(
78
+ message,
79
+ keyid=signing_fingerprint,
80
+ passphrase=passphrase,
81
+ clearsign=True
82
+ )
83
+
84
+ if not result:
85
+ print(f"[ERR] clearsign failed for <{signing_fingerprint[:16]}…>")
86
+ print(f" stderr: {result.stderr}")
87
+ return None
88
+
89
+ print(f"[OK] clearsign({signing_fingerprint[:16]}…)")
90
+ return str(result)
91
+
92
+
93
+ def sign_detached(
94
+ gpg,
95
+ message: str | bytes,
96
+ signing_fingerprint: str,
97
+ *,
98
+ passphrase: str | None = None,
99
+ armor: bool = True
100
+ ) -> str | bytes | None:
101
+ result = gpg.sign(
102
+ message,
103
+ keyid=signing_fingerprint,
104
+ passphrase=passphrase,
105
+ detach=True,
106
+ clearsign=False
107
+ )
108
+
109
+ if not result:
110
+ print(f"[ERR] sign_detached failed for <{signing_fingerprint[:16]}…>")
111
+ print(f" stderr: {result.stderr}")
112
+ return None
113
+
114
+ sig_data = str(result)
115
+ print(f"[OK] sign_detached({signing_fingerprint[:16]}…, armor={armor})")
116
+
117
+ if armor:
118
+ return sig_data
119
+
120
+ return sig_data
121
+
122
+
123
+ def sign_inline(
124
+ gpg,
125
+ message: str | bytes,
126
+ signing_fingerprint: str,
127
+ *,
128
+ passphrase: str | None = None
129
+ ) -> bytes | None:
130
+ result = gpg.sign(
131
+ message,
132
+ keyid=signing_fingerprint,
133
+ passphrase=passphrase,
134
+ detach=False,
135
+ clearsign=False,
136
+ binary=True
137
+ )
138
+
139
+ if not result:
140
+ print(f"[ERR] sign_inline failed for <{signing_fingerprint[:16]}…>")
141
+ return None
142
+
143
+ print(f"[OK] sign_inline({signing_fingerprint[:16]}…)")
144
+ return result.data
145
+
146
+
147
+ def sign_file_detached(
148
+ gpg,
149
+ file_path: Path | str,
150
+ signing_fingerprint: str,
151
+ *,
152
+ passphrase: str | None = None,
153
+ output_path: Path | str | None = None,
154
+ armor: bool = True
155
+ ) -> Path | None:
156
+ src = Path(file_path)
157
+ if not src.exists():
158
+ print(f"[ERR] sign_file_detached: {src} not found.")
159
+ return None
160
+
161
+ ext = ".asc" if armor else ".sig"
162
+ dst = Path(output_path) if output_path else src.with_suffix(src.sufix + ext)
163
+
164
+ with open(src, "rb") as file:
165
+ result = gpg.sign_file(
166
+ file,
167
+ keyid=signing_fingerprint,
168
+ passphrase=passphrase,
169
+ detach=True,
170
+ clearsign=False,
171
+ output=str(dst)
172
+ )
173
+
174
+ if not result:
175
+ print(f"[ERR] sign_file_detached({src.name}): {result.stderr}")
176
+ return None
177
+
178
+ print(f"[OK] sign_file_detached » {dst}")
179
+ return dst
180
+
181
+
182
+ def verify_clearsign(
183
+ gpg,
184
+ signed_message: str,
185
+ *,
186
+ always_trust: bool = True
187
+ ) -> VerificationResult:
188
+ result = gpg.verify(signed_message)
189
+ vr = VerificationResult.from_gnupg(result)
190
+ label = "verify_clearsign"
191
+ if vr.valid:
192
+ print(f"[OK] {label}")
193
+ else:
194
+ print(f"[ERR] {label}")
195
+ print(vr)
196
+ return vr
197
+
198
+
199
+ def verify_detached(
200
+ gpg,
201
+ message: str | bytes,
202
+ signature: str | bytes
203
+ ) -> VerificationResult:
204
+ import tempfile, os
205
+
206
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".asc") as tmp:
207
+ if isinstance(signature, str):
208
+ tmp.write(signature.encode())
209
+ else:
210
+ tmp.write(signature)
211
+ tmp_path = tmp.name
212
+
213
+ try:
214
+ if isinstance(message, str):
215
+ message = message.encode()
216
+
217
+ result = gpg.verify_data(tmp_path, message)
218
+ finally:
219
+ os.unlink(tmp_path)
220
+
221
+ vr = VerificationResult.from_gnupg(result)
222
+ label = "verify_detached"
223
+ if vr.valid:
224
+ print(f"[OK] {label}")
225
+ else:
226
+ print(f"[ERR] {label}")
227
+ print(vr)
228
+ return vr
229
+
230
+
231
+ def verify_detached_file(
232
+ gpg,
233
+ file_path: Path | str,
234
+ sig_path: Path | str
235
+ ) -> VerificationResult:
236
+ src = Path(file_path)
237
+ sig = Path(sig_path)
238
+
239
+ message_bytes = src.read_bytes()
240
+ sig_bytes = sig.read_bytes()
241
+
242
+ return verify_detached(gpg, message_bytes, sig_bytes)
243
+
244
+
245
+ def verify_inline(gpg, signed_data: bytes) -> VerificationResult:
246
+ result = gpg.decrypt(signed_data, always_trust=True)
247
+
248
+ vr = VerificationResult.from_gnupg(result)
249
+ label = "verify_inline"
250
+ if vr.valid:
251
+ print(f"[OK] {label}")
252
+ else:
253
+ print(f"[WARN] {label} - no valid signature found (or unsigned data)")
254
+ print(vr)
255
+ return vr
256
+
257
+
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "artemis_framework"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  description = "A package for doing great things!"
5
5
  authors = ["Dylan Garrett"]
6
6
  license = "MIT"