swarmauri_mre_crypto_pgp 0.3.0.dev3__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.
- swarmauri_mre_crypto_pgp/PGPSealMreCrypto.py +223 -0
- swarmauri_mre_crypto_pgp/__init__.py +11 -0
- swarmauri_mre_crypto_pgp/pgp_mre.py +457 -0
- swarmauri_mre_crypto_pgp/pgp_sealed_cek_mre.py +347 -0
- swarmauri_mre_crypto_pgp-0.3.0.dev3.dist-info/LICENSE +201 -0
- swarmauri_mre_crypto_pgp-0.3.0.dev3.dist-info/METADATA +114 -0
- swarmauri_mre_crypto_pgp-0.3.0.dev3.dist-info/RECORD +9 -0
- swarmauri_mre_crypto_pgp-0.3.0.dev3.dist-info/WHEEL +4 -0
- swarmauri_mre_crypto_pgp-0.3.0.dev3.dist-info/entry_points.txt +10 -0
@@ -0,0 +1,223 @@
|
|
1
|
+
"""OpenPGP sealed-per-recipient MRE provider."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import base64
|
6
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence
|
7
|
+
|
8
|
+
from swarmauri_core.mre_crypto.types import MultiRecipientEnvelope, RecipientId, MreMode
|
9
|
+
from swarmauri_core.crypto.types import Alg, KeyRef
|
10
|
+
from swarmauri_base.mre_crypto.MreCryptoBase import MreCryptoBase
|
11
|
+
from swarmauri_base.ComponentBase import ComponentBase
|
12
|
+
|
13
|
+
try: # pragma: no cover - dependency optional at runtime
|
14
|
+
import pgpy # type: ignore
|
15
|
+
|
16
|
+
_PGP_OK = True
|
17
|
+
except Exception: # pragma: no cover
|
18
|
+
_PGP_OK = False
|
19
|
+
|
20
|
+
|
21
|
+
def _ensure_pgpy() -> None:
|
22
|
+
if not _PGP_OK: # pragma: no cover - env dependent
|
23
|
+
raise RuntimeError(
|
24
|
+
"PGPSealMreCrypto requires 'PGPy'. Install with: pip install pgpy"
|
25
|
+
)
|
26
|
+
|
27
|
+
|
28
|
+
def _load_pubkey(ref: KeyRef) -> "pgpy.PGPKey":
|
29
|
+
_ensure_pgpy()
|
30
|
+
if isinstance(ref, dict):
|
31
|
+
kind = ref.get("kind")
|
32
|
+
if kind == "pgpy_pub" and isinstance(ref.get("pub"), pgpy.PGPKey):
|
33
|
+
return ref["pub"]
|
34
|
+
if kind == "pgpy_pub_armored" and isinstance(ref.get("pub"), str):
|
35
|
+
k, _ = pgpy.PGPKey.from_blob(ref["pub"])
|
36
|
+
return k
|
37
|
+
raise TypeError("Unsupported recipient KeyRef for PGP public key.")
|
38
|
+
|
39
|
+
|
40
|
+
def _load_privkey(ref: KeyRef, passphrase: Optional[bytes | str]) -> "pgpy.PGPKey":
|
41
|
+
_ensure_pgpy()
|
42
|
+
if isinstance(ref, dict):
|
43
|
+
kind = ref.get("kind")
|
44
|
+
if kind == "pgpy_priv" and isinstance(ref.get("priv"), pgpy.PGPKey):
|
45
|
+
k: pgpy.PGPKey = ref["priv"]
|
46
|
+
elif kind == "pgpy_priv_armored" and isinstance(ref.get("priv"), str):
|
47
|
+
k, _ = pgpy.PGPKey.from_blob(ref["priv"])
|
48
|
+
else:
|
49
|
+
raise TypeError("Unsupported identity KeyRef for PGP private key.")
|
50
|
+
if not k.is_unlocked:
|
51
|
+
if passphrase is None:
|
52
|
+
raise RuntimeError(
|
53
|
+
"PGP private key is locked; supply opts['passphrase']."
|
54
|
+
)
|
55
|
+
k.unlock(passphrase)
|
56
|
+
return k
|
57
|
+
raise TypeError("Unsupported identity KeyRef for PGP private key.")
|
58
|
+
|
59
|
+
|
60
|
+
def _fingerprint_pub(k: "pgpy.PGPKey") -> str:
|
61
|
+
return str(k.fingerprint)
|
62
|
+
|
63
|
+
|
64
|
+
def _pgp_encrypt_bytes_for(pub: "pgpy.PGPKey", data: bytes) -> bytes:
|
65
|
+
"""Encrypt bytes for a recipient using OpenPGP."""
|
66
|
+
_ensure_pgpy()
|
67
|
+
msg = pgpy.PGPMessage.new(base64.b64encode(data), file=False)
|
68
|
+
enc = pub.encrypt(msg)
|
69
|
+
return bytes(enc.__bytes__())
|
70
|
+
|
71
|
+
|
72
|
+
def _pgp_decrypt_bytes_with(priv: "pgpy.PGPKey", blob: bytes) -> bytes:
|
73
|
+
_ensure_pgpy()
|
74
|
+
msg = pgpy.PGPMessage.from_blob(blob)
|
75
|
+
dec = priv.decrypt(msg)
|
76
|
+
if isinstance(dec.message, str):
|
77
|
+
return base64.b64decode(dec.message.encode("utf-8"))
|
78
|
+
return base64.b64decode(dec.message)
|
79
|
+
|
80
|
+
|
81
|
+
def _make_sealed_recipient(rid: str, sealed_payload: bytes) -> Dict[str, Any]:
|
82
|
+
return {"id": rid, "sealed": sealed_payload}
|
83
|
+
|
84
|
+
|
85
|
+
@ComponentBase.register_type(MreCryptoBase, "PGPSealMreCrypto")
|
86
|
+
class PGPSealMreCrypto(MreCryptoBase):
|
87
|
+
"""OpenPGP sealed-per-recipient MRE provider."""
|
88
|
+
|
89
|
+
type: str = "PGPSealMreCrypto"
|
90
|
+
|
91
|
+
def supports(self) -> Dict[str, Iterable[str | MreMode]]:
|
92
|
+
return {
|
93
|
+
"recipient": ("OpenPGP-SEAL",),
|
94
|
+
"modes": (MreMode.SEALED_PER_RECIPIENT,),
|
95
|
+
"features": (),
|
96
|
+
}
|
97
|
+
|
98
|
+
async def encrypt_for_many(
|
99
|
+
self,
|
100
|
+
recipients: Sequence[KeyRef],
|
101
|
+
pt: bytes,
|
102
|
+
*,
|
103
|
+
payload_alg: Optional[Alg] = None,
|
104
|
+
recipient_alg: Optional[Alg] = None,
|
105
|
+
mode: Optional[MreMode | str] = None,
|
106
|
+
aad: Optional[bytes] = None,
|
107
|
+
shared: Optional[Mapping[str, bytes]] = None,
|
108
|
+
opts: Optional[Mapping[str, object]] = None,
|
109
|
+
) -> MultiRecipientEnvelope:
|
110
|
+
_ensure_pgpy()
|
111
|
+
m = (
|
112
|
+
MreMode(mode)
|
113
|
+
if isinstance(mode, str)
|
114
|
+
else (mode or MreMode.SEALED_PER_RECIPIENT)
|
115
|
+
)
|
116
|
+
if m != MreMode.SEALED_PER_RECIPIENT:
|
117
|
+
raise ValueError(
|
118
|
+
f"PGPSealMreCrypto supports only mode={MreMode.SEALED_PER_RECIPIENT.value}."
|
119
|
+
)
|
120
|
+
if aad is not None:
|
121
|
+
raise ValueError("AAD is not supported in sealed_per_recipient mode.")
|
122
|
+
|
123
|
+
pubs = [_load_pubkey(r) for r in recipients]
|
124
|
+
rids = [_fingerprint_pub(pk) for pk in pubs]
|
125
|
+
sealed_entries = [
|
126
|
+
_make_sealed_recipient(rid, _pgp_encrypt_bytes_for(pk, pt))
|
127
|
+
for rid, pk in zip(rids, pubs)
|
128
|
+
]
|
129
|
+
env: MultiRecipientEnvelope = {
|
130
|
+
"mode": MreMode.SEALED_PER_RECIPIENT.value,
|
131
|
+
"recipient_alg": "OpenPGP-SEAL",
|
132
|
+
"payload": {"kind": "sealed_per_recipient"},
|
133
|
+
"recipients": sealed_entries,
|
134
|
+
}
|
135
|
+
if shared:
|
136
|
+
env["shared"] = dict(shared)
|
137
|
+
return env
|
138
|
+
|
139
|
+
async def open_for(
|
140
|
+
self,
|
141
|
+
my_identity: KeyRef,
|
142
|
+
env: MultiRecipientEnvelope,
|
143
|
+
*,
|
144
|
+
aad: Optional[bytes] = None,
|
145
|
+
opts: Optional[Mapping[str, object]] = None,
|
146
|
+
) -> bytes:
|
147
|
+
_ensure_pgpy()
|
148
|
+
if env.get("mode") != MreMode.SEALED_PER_RECIPIENT.value:
|
149
|
+
raise ValueError("Envelope mode mismatch; expected sealed_per_recipient.")
|
150
|
+
if aad is not None:
|
151
|
+
raise ValueError("AAD is not supported in sealed_per_recipient mode.")
|
152
|
+
|
153
|
+
priv = _load_privkey(my_identity, (opts or {}).get("passphrase"))
|
154
|
+
my_id = str(priv.fingerprint)
|
155
|
+
entries: List[Dict[str, Any]] = env.get("recipients", [])
|
156
|
+
target = next((e for e in entries if e.get("id") == my_id), None)
|
157
|
+
if target:
|
158
|
+
return _pgp_decrypt_bytes_with(priv, target["sealed"])
|
159
|
+
for e in entries:
|
160
|
+
try:
|
161
|
+
return _pgp_decrypt_bytes_with(priv, e["sealed"])
|
162
|
+
except Exception:
|
163
|
+
continue
|
164
|
+
raise PermissionError("This identity cannot open the sealed envelope.")
|
165
|
+
|
166
|
+
async def open_for_many(
|
167
|
+
self,
|
168
|
+
my_identities: Sequence[KeyRef],
|
169
|
+
env: MultiRecipientEnvelope,
|
170
|
+
*,
|
171
|
+
aad: Optional[bytes] = None,
|
172
|
+
opts: Optional[Mapping[str, object]] = None,
|
173
|
+
) -> bytes:
|
174
|
+
last_err: Optional[Exception] = None
|
175
|
+
for ident in my_identities:
|
176
|
+
try:
|
177
|
+
return await self.open_for(ident, env, aad=aad, opts=opts)
|
178
|
+
except Exception as e: # pragma: no cover - best effort
|
179
|
+
last_err = e
|
180
|
+
continue
|
181
|
+
raise last_err or PermissionError(
|
182
|
+
"None of the provided identities could open the envelope."
|
183
|
+
)
|
184
|
+
|
185
|
+
async def rewrap(
|
186
|
+
self,
|
187
|
+
env: MultiRecipientEnvelope,
|
188
|
+
*,
|
189
|
+
add: Optional[Sequence[KeyRef]] = None,
|
190
|
+
remove: Optional[Sequence[RecipientId]] = None,
|
191
|
+
recipient_alg: Optional[Alg] = None,
|
192
|
+
opts: Optional[Mapping[str, object]] = None,
|
193
|
+
) -> MultiRecipientEnvelope:
|
194
|
+
_ensure_pgpy()
|
195
|
+
if env.get("mode") != MreMode.SEALED_PER_RECIPIENT.value:
|
196
|
+
raise ValueError("Envelope mode mismatch; expected sealed_per_recipient.")
|
197
|
+
|
198
|
+
add = add or ()
|
199
|
+
remove_ids = set(remove or ())
|
200
|
+
new_env: MultiRecipientEnvelope = {k: v for k, v in env.items()}
|
201
|
+
current_entries: List[Dict[str, Any]] = list(new_env.get("recipients", []))
|
202
|
+
if remove_ids:
|
203
|
+
current_entries = [
|
204
|
+
e for e in current_entries if e.get("id") not in remove_ids
|
205
|
+
]
|
206
|
+
if add:
|
207
|
+
pt_bytes = (opts or {}).get("plaintext")
|
208
|
+
if not isinstance(pt_bytes, (bytes, bytearray)):
|
209
|
+
raise RuntimeError(
|
210
|
+
"Rewrap(add=...) in sealed_per_recipient mode requires opts['plaintext'] (bytes)."
|
211
|
+
)
|
212
|
+
pubs = [_load_pubkey(r) for r in add]
|
213
|
+
rids = [_fingerprint_pub(pk) for pk in pubs]
|
214
|
+
new_entries = [
|
215
|
+
_make_sealed_recipient(rid, _pgp_encrypt_bytes_for(pk, pt_bytes))
|
216
|
+
for rid, pk in zip(rids, pubs)
|
217
|
+
]
|
218
|
+
remaining = [
|
219
|
+
e for e in current_entries if e["id"] not in {rid for rid in rids}
|
220
|
+
]
|
221
|
+
current_entries = remaining + new_entries
|
222
|
+
new_env["recipients"] = current_entries
|
223
|
+
return new_env
|
@@ -0,0 +1,11 @@
|
|
1
|
+
"""Package exports for the PGP MRE crypto providers."""
|
2
|
+
|
3
|
+
from .PGPSealMreCrypto import PGPSealMreCrypto
|
4
|
+
from .pgp_sealed_cek_mre import PGPSealedCekMreCrypto
|
5
|
+
from .pgp_mre import PGPMreCrypto
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
"PGPSealMreCrypto",
|
9
|
+
"PGPSealedCekMreCrypto",
|
10
|
+
"PGPMreCrypto",
|
11
|
+
]
|
@@ -0,0 +1,457 @@
|
|
1
|
+
"""PGPMreCrypto: OpenPGP-backed MRE provider supporting multiple modes."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import base64
|
6
|
+
import os
|
7
|
+
from typing import (
|
8
|
+
Any,
|
9
|
+
Dict,
|
10
|
+
Iterable,
|
11
|
+
List,
|
12
|
+
Literal,
|
13
|
+
Mapping,
|
14
|
+
Optional,
|
15
|
+
Sequence,
|
16
|
+
Tuple,
|
17
|
+
)
|
18
|
+
|
19
|
+
from swarmauri_core.mre_crypto.types import MultiRecipientEnvelope, RecipientId, MreMode
|
20
|
+
from swarmauri_core.crypto.types import Alg, KeyRef
|
21
|
+
from swarmauri_base.mre_crypto.MreCryptoBase import MreCryptoBase
|
22
|
+
from swarmauri_base.ComponentBase import ComponentBase
|
23
|
+
|
24
|
+
try: # pragma: no cover - dependency optional at runtime
|
25
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
26
|
+
|
27
|
+
_CRYPTOGRAPHY_OK = True
|
28
|
+
except Exception: # pragma: no cover
|
29
|
+
_CRYPTOGRAPHY_OK = False
|
30
|
+
|
31
|
+
try: # pragma: no cover - dependency optional at runtime
|
32
|
+
import pgpy
|
33
|
+
|
34
|
+
_PGP_OK = True
|
35
|
+
except Exception: # pragma: no cover
|
36
|
+
_PGP_OK = False
|
37
|
+
|
38
|
+
|
39
|
+
def _ensure_crypto() -> None:
|
40
|
+
if not _CRYPTOGRAPHY_OK:
|
41
|
+
raise RuntimeError(
|
42
|
+
"PGPMreCrypto requires 'cryptography'. Install with: pip install cryptography"
|
43
|
+
)
|
44
|
+
|
45
|
+
|
46
|
+
def _ensure_pgpy() -> None:
|
47
|
+
if not _PGP_OK:
|
48
|
+
raise RuntimeError(
|
49
|
+
"PGPMreCrypto requires 'PGPy'. Install with: pip install pgpy"
|
50
|
+
)
|
51
|
+
|
52
|
+
|
53
|
+
# ---------------------------------------------------------------------------
|
54
|
+
# PGP helpers
|
55
|
+
# ---------------------------------------------------------------------------
|
56
|
+
|
57
|
+
|
58
|
+
def _load_pubkey(ref: KeyRef) -> "pgpy.PGPKey":
|
59
|
+
_ensure_pgpy()
|
60
|
+
if isinstance(ref, dict):
|
61
|
+
kind = ref.get("kind")
|
62
|
+
if kind == "pgpy_pub" and isinstance(ref.get("pub"), pgpy.PGPKey):
|
63
|
+
return ref["pub"]
|
64
|
+
if kind == "pgpy_pub_armored" and isinstance(ref.get("pub"), str):
|
65
|
+
k, _ = pgpy.PGPKey.from_blob(ref["pub"])
|
66
|
+
return k
|
67
|
+
raise TypeError("Unsupported recipient KeyRef for PGP public key.")
|
68
|
+
|
69
|
+
|
70
|
+
def _load_privkey(ref: KeyRef, passphrase: Optional[bytes | str]) -> "pgpy.PGPKey":
|
71
|
+
_ensure_pgpy()
|
72
|
+
if isinstance(ref, dict):
|
73
|
+
kind = ref.get("kind")
|
74
|
+
if kind == "pgpy_priv" and isinstance(ref.get("priv"), pgpy.PGPKey):
|
75
|
+
k: pgpy.PGPKey = ref["priv"]
|
76
|
+
elif kind == "pgpy_priv_armored" and isinstance(ref.get("priv"), str):
|
77
|
+
k, _ = pgpy.PGPKey.from_blob(ref["priv"])
|
78
|
+
else:
|
79
|
+
raise TypeError("Unsupported identity KeyRef for PGP private key.")
|
80
|
+
if not k.is_unlocked:
|
81
|
+
if passphrase is None:
|
82
|
+
raise RuntimeError(
|
83
|
+
"PGP private key is locked; supply opts['passphrase']."
|
84
|
+
)
|
85
|
+
k.unlock(passphrase)
|
86
|
+
return k
|
87
|
+
raise TypeError("Unsupported identity KeyRef for PGP private key.")
|
88
|
+
|
89
|
+
|
90
|
+
def _fingerprint_pub(k: "pgpy.PGPKey") -> str:
|
91
|
+
return str(k.fingerprint)
|
92
|
+
|
93
|
+
|
94
|
+
def _pgp_encrypt_bytes_for(pub: "pgpy.PGPKey", data: bytes) -> bytes:
|
95
|
+
_ensure_pgpy()
|
96
|
+
msg = pgpy.PGPMessage.new(base64.b64encode(data), file=False)
|
97
|
+
enc = pub.encrypt(msg)
|
98
|
+
return bytes(enc.__bytes__())
|
99
|
+
|
100
|
+
|
101
|
+
def _pgp_decrypt_bytes_with(priv: "pgpy.PGPKey", blob: bytes) -> bytes:
|
102
|
+
_ensure_pgpy()
|
103
|
+
msg = pgpy.PGPMessage.from_blob(blob)
|
104
|
+
dec = priv.decrypt(msg)
|
105
|
+
if isinstance(dec.message, str):
|
106
|
+
return base64.b64decode(dec.message.encode("utf-8"))
|
107
|
+
return base64.b64decode(dec.message)
|
108
|
+
|
109
|
+
|
110
|
+
# ---------------------------------------------------------------------------
|
111
|
+
# AEAD helpers
|
112
|
+
# ---------------------------------------------------------------------------
|
113
|
+
|
114
|
+
|
115
|
+
def _aead_encrypt_gcm(
|
116
|
+
cek: bytes, pt: bytes, *, aad: Optional[bytes]
|
117
|
+
) -> Tuple[bytes, bytes, bytes]:
|
118
|
+
_ensure_crypto()
|
119
|
+
if len(cek) != 32:
|
120
|
+
raise ValueError("AES-256-GCM requires a 32-byte CEK.")
|
121
|
+
aead = AESGCM(cek)
|
122
|
+
nonce = os.urandom(12)
|
123
|
+
cttag = aead.encrypt(nonce, pt, aad)
|
124
|
+
return nonce, cttag[:-16], cttag[-16:]
|
125
|
+
|
126
|
+
|
127
|
+
def _aead_decrypt_gcm(
|
128
|
+
cek: bytes, nonce: bytes, ct: bytes, tag: bytes, *, aad: Optional[bytes]
|
129
|
+
) -> bytes:
|
130
|
+
_ensure_crypto()
|
131
|
+
aead = AESGCM(cek)
|
132
|
+
return aead.decrypt(nonce, ct + tag, aad)
|
133
|
+
|
134
|
+
|
135
|
+
def _make_aead_payload(
|
136
|
+
payload_alg: str, nonce: bytes, ct: bytes, tag: bytes, aad: Optional[bytes]
|
137
|
+
) -> Dict[str, Any]:
|
138
|
+
return {
|
139
|
+
"kind": "aead",
|
140
|
+
"alg": payload_alg,
|
141
|
+
"nonce": nonce,
|
142
|
+
"ct": ct,
|
143
|
+
"tag": tag,
|
144
|
+
"aad": aad,
|
145
|
+
}
|
146
|
+
|
147
|
+
|
148
|
+
def _make_recipient_header(rid: str, header: bytes) -> Dict[str, Any]:
|
149
|
+
return {"id": rid, "header": header}
|
150
|
+
|
151
|
+
|
152
|
+
def _make_sealed_recipient(rid: str, sealed_payload: bytes) -> Dict[str, Any]:
|
153
|
+
return {"id": rid, "sealed": sealed_payload}
|
154
|
+
|
155
|
+
|
156
|
+
@ComponentBase.register_type(MreCryptoBase, "PGPMreCrypto")
|
157
|
+
class PGPMreCrypto(MreCryptoBase):
|
158
|
+
"""OpenPGP-backed multi-recipient encryption provider."""
|
159
|
+
|
160
|
+
type: Literal["PGPMreCrypto"] = "PGPMreCrypto"
|
161
|
+
default_payload_alg: str = "AES-256-GCM"
|
162
|
+
default_recipient_alg: str = "OpenPGP"
|
163
|
+
|
164
|
+
def supports(
|
165
|
+
self,
|
166
|
+
) -> Dict[str, Iterable[str | MreMode]]: # pragma: no cover - trivial
|
167
|
+
modes: Tuple[str | MreMode, ...] = (
|
168
|
+
MreMode.ENC_ONCE_HEADERS,
|
169
|
+
MreMode.SEALED_PER_RECIPIENT,
|
170
|
+
)
|
171
|
+
return {
|
172
|
+
"payload": (self.default_payload_alg,),
|
173
|
+
"recipient": ("OpenPGP", "OpenPGP-SEAL"),
|
174
|
+
"modes": modes,
|
175
|
+
"features": ("aad", "rewrap_without_reencrypt"),
|
176
|
+
}
|
177
|
+
|
178
|
+
async def encrypt_for_many(
|
179
|
+
self,
|
180
|
+
recipients: Sequence[KeyRef],
|
181
|
+
pt: bytes,
|
182
|
+
*,
|
183
|
+
payload_alg: Optional[Alg] = None,
|
184
|
+
recipient_alg: Optional[Alg] = None,
|
185
|
+
mode: Optional[MreMode | str] = None,
|
186
|
+
aad: Optional[bytes] = None,
|
187
|
+
shared: Optional[Mapping[str, bytes]] = None,
|
188
|
+
opts: Optional[Mapping[str, object]] = None,
|
189
|
+
) -> MultiRecipientEnvelope:
|
190
|
+
_ensure_pgpy()
|
191
|
+
|
192
|
+
m = (
|
193
|
+
MreMode(mode)
|
194
|
+
if isinstance(mode, str)
|
195
|
+
else (mode or MreMode.ENC_ONCE_HEADERS)
|
196
|
+
)
|
197
|
+
payload_alg = payload_alg or self.default_payload_alg
|
198
|
+
recipient_alg = recipient_alg or self.default_recipient_alg
|
199
|
+
|
200
|
+
pubs = [_load_pubkey(r) for r in recipients]
|
201
|
+
rids = [_fingerprint_pub(pk) for pk in pubs]
|
202
|
+
|
203
|
+
if m == MreMode.ENC_ONCE_HEADERS:
|
204
|
+
if payload_alg != "AES-256-GCM":
|
205
|
+
raise ValueError(
|
206
|
+
"PGPMreCrypto currently supports only AES-256-GCM for shared payload."
|
207
|
+
)
|
208
|
+
cek = os.urandom(32)
|
209
|
+
nonce, ct, tag = _aead_encrypt_gcm(cek, pt, aad=aad)
|
210
|
+
headers = [
|
211
|
+
_make_recipient_header(rid, _pgp_encrypt_bytes_for(pk, cek))
|
212
|
+
for rid, pk in zip(rids, pubs)
|
213
|
+
]
|
214
|
+
env: MultiRecipientEnvelope = {
|
215
|
+
"mode": MreMode.ENC_ONCE_HEADERS.value,
|
216
|
+
"payload_alg": payload_alg,
|
217
|
+
"recipient_alg": "OpenPGP",
|
218
|
+
"payload": _make_aead_payload(payload_alg, nonce, ct, tag, aad),
|
219
|
+
"recipients": headers,
|
220
|
+
}
|
221
|
+
if shared:
|
222
|
+
env["shared"] = dict(shared)
|
223
|
+
return env
|
224
|
+
|
225
|
+
if m == MreMode.SEALED_PER_RECIPIENT:
|
226
|
+
sealed_entries = [
|
227
|
+
_make_sealed_recipient(rid, _pgp_encrypt_bytes_for(pk, pt))
|
228
|
+
for rid, pk in zip(rids, pubs)
|
229
|
+
]
|
230
|
+
env = {
|
231
|
+
"mode": MreMode.SEALED_PER_RECIPIENT.value,
|
232
|
+
"recipient_alg": "OpenPGP-SEAL",
|
233
|
+
"payload": {"kind": "sealed_per_recipient"},
|
234
|
+
"recipients": sealed_entries,
|
235
|
+
}
|
236
|
+
if shared:
|
237
|
+
env["shared"] = dict(shared)
|
238
|
+
return env
|
239
|
+
|
240
|
+
raise ValueError(f"Unsupported mode: {mode}")
|
241
|
+
|
242
|
+
async def open_for(
|
243
|
+
self,
|
244
|
+
my_identity: KeyRef,
|
245
|
+
env: MultiRecipientEnvelope,
|
246
|
+
*,
|
247
|
+
aad: Optional[bytes] = None,
|
248
|
+
opts: Optional[Mapping[str, object]] = None,
|
249
|
+
) -> bytes:
|
250
|
+
_ensure_pgpy()
|
251
|
+
mode_str = env.get("mode")
|
252
|
+
m = MreMode(mode_str) if isinstance(mode_str, str) else mode_str
|
253
|
+
priv = _load_privkey(my_identity, (opts or {}).get("passphrase"))
|
254
|
+
|
255
|
+
if m == MreMode.ENC_ONCE_HEADERS:
|
256
|
+
my_id = str(priv.fingerprint)
|
257
|
+
headers: List[Dict[str, Any]] = env.get("recipients", [])
|
258
|
+
target = next((h for h in headers if h.get("id") == my_id), None)
|
259
|
+
|
260
|
+
cek: Optional[bytes] = None
|
261
|
+
if target:
|
262
|
+
cek = _pgp_decrypt_bytes_with(priv, target["header"])
|
263
|
+
else:
|
264
|
+
for h in headers:
|
265
|
+
try:
|
266
|
+
cek = _pgp_decrypt_bytes_with(priv, h["header"])
|
267
|
+
break
|
268
|
+
except Exception:
|
269
|
+
continue
|
270
|
+
if cek is None:
|
271
|
+
raise PermissionError("This identity cannot open the envelope.")
|
272
|
+
|
273
|
+
payload = env["payload"]
|
274
|
+
if payload.get("alg") != "AES-256-GCM":
|
275
|
+
raise ValueError("Unsupported payload_alg for PGPMreCrypto open.")
|
276
|
+
nonce, ct, tag = payload["nonce"], payload["ct"], payload["tag"]
|
277
|
+
bound_aad = payload.get("aad", None)
|
278
|
+
if aad is not None and bound_aad is not None and aad != bound_aad:
|
279
|
+
raise ValueError("AAD mismatch.")
|
280
|
+
return _aead_decrypt_gcm(cek, nonce, ct, tag, aad=bound_aad)
|
281
|
+
|
282
|
+
if m == MreMode.SEALED_PER_RECIPIENT:
|
283
|
+
my_id = str(priv.fingerprint)
|
284
|
+
entries: List[Dict[str, Any]] = env.get("recipients", [])
|
285
|
+
target = next((e for e in entries if e.get("id") == my_id), None)
|
286
|
+
if not target:
|
287
|
+
for e in entries:
|
288
|
+
try:
|
289
|
+
return _pgp_decrypt_bytes_with(priv, e["sealed"])
|
290
|
+
except Exception:
|
291
|
+
continue
|
292
|
+
raise PermissionError("This identity cannot open the sealed envelope.")
|
293
|
+
return _pgp_decrypt_bytes_with(priv, target["sealed"])
|
294
|
+
|
295
|
+
raise ValueError(f"Unsupported mode: {mode_str}")
|
296
|
+
|
297
|
+
async def open_for_many(
|
298
|
+
self,
|
299
|
+
my_identities: Sequence[KeyRef],
|
300
|
+
env: MultiRecipientEnvelope,
|
301
|
+
*,
|
302
|
+
aad: Optional[bytes] = None,
|
303
|
+
opts: Optional[Mapping[str, object]] = None,
|
304
|
+
) -> bytes:
|
305
|
+
last_err: Optional[Exception] = None
|
306
|
+
for ident in my_identities:
|
307
|
+
try:
|
308
|
+
return await self.open_for(ident, env, aad=aad, opts=opts)
|
309
|
+
except Exception as e: # pragma: no cover
|
310
|
+
last_err = e
|
311
|
+
continue
|
312
|
+
raise last_err or PermissionError(
|
313
|
+
"None of the provided identities could open the envelope."
|
314
|
+
)
|
315
|
+
|
316
|
+
async def rewrap(
|
317
|
+
self,
|
318
|
+
env: MultiRecipientEnvelope,
|
319
|
+
*,
|
320
|
+
add: Optional[Sequence[KeyRef]] = None,
|
321
|
+
remove: Optional[Sequence[RecipientId]] = None,
|
322
|
+
recipient_alg: Optional[Alg] = None,
|
323
|
+
opts: Optional[Mapping[str, object]] = None,
|
324
|
+
) -> MultiRecipientEnvelope:
|
325
|
+
_ensure_pgpy()
|
326
|
+
mode_str = env.get("mode")
|
327
|
+
m = MreMode(mode_str) if isinstance(mode_str, str) else mode_str
|
328
|
+
|
329
|
+
add = add or ()
|
330
|
+
remove_ids = set(remove or ())
|
331
|
+
|
332
|
+
new_env: MultiRecipientEnvelope = {k: v for k, v in env.items()}
|
333
|
+
|
334
|
+
if m == MreMode.ENC_ONCE_HEADERS:
|
335
|
+
current_headers: List[Dict[str, Any]] = list(new_env.get("recipients", []))
|
336
|
+
if remove_ids:
|
337
|
+
current_headers = [
|
338
|
+
h for h in current_headers if h.get("id") not in remove_ids
|
339
|
+
]
|
340
|
+
|
341
|
+
cek: Optional[bytes] = None
|
342
|
+
if add:
|
343
|
+
if opts and isinstance(opts.get("cek"), (bytes, bytearray)):
|
344
|
+
cek = bytes(opts["cek"])
|
345
|
+
elif opts and opts.get("manage_key"):
|
346
|
+
manage_key = _load_privkey(
|
347
|
+
opts["manage_key"], (opts or {}).get("passphrase")
|
348
|
+
)
|
349
|
+
candidate_headers = current_headers or list(
|
350
|
+
env.get("recipients", [])
|
351
|
+
)
|
352
|
+
for h in candidate_headers:
|
353
|
+
try:
|
354
|
+
cek = _pgp_decrypt_bytes_with(manage_key, h["header"])
|
355
|
+
break
|
356
|
+
except Exception:
|
357
|
+
continue
|
358
|
+
else:
|
359
|
+
raise RuntimeError(
|
360
|
+
"Rewrap(add=...) requires opts['cek'] or opts['manage_key']."
|
361
|
+
)
|
362
|
+
|
363
|
+
if add and cek is not None:
|
364
|
+
pubs = [_load_pubkey(r) for r in add]
|
365
|
+
rids = [_fingerprint_pub(pk) for pk in pubs]
|
366
|
+
new_headers = [
|
367
|
+
_make_recipient_header(rid, _pgp_encrypt_bytes_for(pk, cek))
|
368
|
+
for rid, pk in zip(rids, pubs)
|
369
|
+
]
|
370
|
+
merged: List[Dict[str, Any]] = []
|
371
|
+
for h in current_headers:
|
372
|
+
if h["id"] not in {rid for rid in rids}:
|
373
|
+
merged.append(h)
|
374
|
+
merged.extend(new_headers)
|
375
|
+
current_headers = merged
|
376
|
+
|
377
|
+
rotate = bool(opts.get("rotate_payload_on_revoke")) if opts else False
|
378
|
+
if remove_ids and rotate:
|
379
|
+
if not cek:
|
380
|
+
if opts and opts.get("manage_key"):
|
381
|
+
manage_key = _load_privkey(
|
382
|
+
opts["manage_key"], (opts or {}).get("passphrase")
|
383
|
+
)
|
384
|
+
for h in env.get("recipients", []):
|
385
|
+
try:
|
386
|
+
cek = _pgp_decrypt_bytes_with(manage_key, h["header"])
|
387
|
+
break
|
388
|
+
except Exception:
|
389
|
+
continue
|
390
|
+
if not cek:
|
391
|
+
raise RuntimeError(
|
392
|
+
"rotate_payload_on_revoke requires CEK via opts['manage_key'] or opts['cek']."
|
393
|
+
)
|
394
|
+
|
395
|
+
new_cek = os.urandom(32)
|
396
|
+
payload = env["payload"]
|
397
|
+
old_pt = _aead_decrypt_gcm(
|
398
|
+
cek,
|
399
|
+
payload["nonce"],
|
400
|
+
payload["ct"],
|
401
|
+
payload["tag"],
|
402
|
+
aad=payload.get("aad"),
|
403
|
+
)
|
404
|
+
nonce, ct, tag = _aead_encrypt_gcm(
|
405
|
+
new_cek, old_pt, aad=payload.get("aad")
|
406
|
+
)
|
407
|
+
new_env["payload"] = _make_aead_payload(
|
408
|
+
env.get("payload_alg", self.default_payload_alg),
|
409
|
+
nonce,
|
410
|
+
ct,
|
411
|
+
tag,
|
412
|
+
payload.get("aad"),
|
413
|
+
)
|
414
|
+
|
415
|
+
add_pubkeys = (opts or {}).get("add_pubkeys")
|
416
|
+
if not isinstance(add_pubkeys, (list, tuple)) or not add_pubkeys:
|
417
|
+
raise RuntimeError(
|
418
|
+
"After rotation, provide opts['add_pubkeys'] = [KeyRef(pub=...), ...] for remaining recipients."
|
419
|
+
)
|
420
|
+
pubs = [_load_pubkey(r) for r in add_pubkeys]
|
421
|
+
rids = [_fingerprint_pub(pk) for pk in pubs]
|
422
|
+
new_headers = [
|
423
|
+
_make_recipient_header(rid, _pgp_encrypt_bytes_for(pk, new_cek))
|
424
|
+
for rid, pk in zip(rids, pubs)
|
425
|
+
]
|
426
|
+
current_headers = new_headers
|
427
|
+
|
428
|
+
new_env["recipients"] = current_headers
|
429
|
+
return new_env
|
430
|
+
|
431
|
+
if m == MreMode.SEALED_PER_RECIPIENT:
|
432
|
+
current_entries: List[Dict[str, Any]] = list(new_env.get("recipients", []))
|
433
|
+
if remove_ids:
|
434
|
+
current_entries = [
|
435
|
+
e for e in current_entries if e.get("id") not in remove_ids
|
436
|
+
]
|
437
|
+
if add:
|
438
|
+
pt_bytes = (opts or {}).get("plaintext")
|
439
|
+
if not isinstance(pt_bytes, (bytes, bytearray)):
|
440
|
+
raise RuntimeError(
|
441
|
+
"Rewrap(add=...) in sealed_per_recipient mode requires opts['plaintext']."
|
442
|
+
)
|
443
|
+
pubs = [_load_pubkey(r) for r in add]
|
444
|
+
rids = [_fingerprint_pub(pk) for pk in pubs]
|
445
|
+
new_entries = [
|
446
|
+
_make_sealed_recipient(rid, _pgp_encrypt_bytes_for(pk, pt_bytes))
|
447
|
+
for rid, pk in zip(rids, pubs)
|
448
|
+
]
|
449
|
+
remaining = [
|
450
|
+
e for e in current_entries if e["id"] not in {rid for rid in rids}
|
451
|
+
]
|
452
|
+
current_entries = remaining + new_entries
|
453
|
+
|
454
|
+
new_env["recipients"] = current_entries
|
455
|
+
return new_env
|
456
|
+
|
457
|
+
raise ValueError(f"Unsupported mode for rewrap: {mode_str}")
|