-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdecrypt_text.py
More file actions
213 lines (173 loc) · 6.79 KB
/
Copy pathdecrypt_text.py
File metadata and controls
213 lines (173 loc) · 6.79 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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
#!/usr/bin/env python3
"""
Decrypt a Base64-encoded text value using a Base64-encoded AES-256 key.
Usage:
python3 decrypt_text.py \
--key '<base64_plain_key>' \
--data '<base64_encrypted_text>'
python3 decrypt_text.py \
--key '<base64_plain_key>' \
--data '<base64_encrypted_text>' \
--debug
Required dependency:
pip3 install cryptography
Encrypted payload format:
byte[0] : IV length
byte[1..N] : IV
next 2 bytes : AAD length (little-endian)
next M bytes : AAD
next 4 bytes : cipher length (little-endian)
next K bytes : cipher text
next 16 bytes: GCM authentication tag
This script:
- decodes the key and encrypted value from Base64
- parses the encrypted payload
- decrypts the content using AES-GCM-256
- prints the decrypted plain text
"""
import argparse
import base64
import binascii
import sys
from cryptography.exceptions import InvalidTag
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
TAG_LEN = 16
class DecryptError(Exception):
"""Raised when the encrypted payload cannot be decrypted safely."""
def b64decode_or_raise(value: str, field_name: str) -> bytes:
"""Decode a Base64 string and raise a readable error on failure."""
try:
return base64.b64decode(value, validate=True)
except binascii.Error as exc:
raise DecryptError(f"{field_name} is not valid Base64: {exc}") from exc
def require_length(data: bytes, needed: int, field_name: str) -> None:
"""Ensure enough bytes remain before reading the next payload field."""
if len(data) < needed:
raise DecryptError(
f"Encrypted payload is too short while reading {field_name}: "
f"needed at least {needed} bytes, got {len(data)}"
)
def parse_segment_payload(encrypted: bytes) -> dict:
"""Parse the encrypted payload into IV, AAD, cipher text, and tag."""
offset = 0
require_length(encrypted, 1, "iv length")
iv_len = encrypted[offset]
offset += 1
require_length(encrypted[offset:], iv_len, "iv")
iv = encrypted[offset:offset + iv_len]
offset += iv_len
require_length(encrypted[offset:], 2, "aad length")
aad_len = int.from_bytes(encrypted[offset:offset + 2], byteorder="little")
offset += 2
require_length(encrypted[offset:], aad_len, "aad")
aad = encrypted[offset:offset + aad_len]
offset += aad_len
require_length(encrypted[offset:], 4, "cipher length")
cipher_len = int.from_bytes(encrypted[offset:offset + 4], byteorder="little")
offset += 4
require_length(encrypted[offset:], cipher_len, "cipher text")
cipher = encrypted[offset:offset + cipher_len]
offset += cipher_len
require_length(encrypted[offset:], TAG_LEN, "GCM tag")
tag = encrypted[offset:offset + TAG_LEN]
offset += TAG_LEN
if offset != len(encrypted):
extra = len(encrypted) - offset
raise DecryptError(
f"Encrypted payload has unexpected trailing bytes: {extra} extra byte(s) found"
)
return {
"iv_len": iv_len,
"aad_len": aad_len,
"cipher_len": cipher_len,
"total_len": len(encrypted),
"iv": iv,
"aad": aad,
"cipher": cipher,
"tag": tag,
}
def decrypt_text(plain_key_b64: str, encrypted_content_b64: str) -> dict:
"""Decrypt the text payload and return parsed metadata plus plaintext."""
plain_key = b64decode_or_raise(plain_key_b64, "Plain key")
encrypted = b64decode_or_raise(encrypted_content_b64, "Encrypted content")
if len(plain_key) != 32:
raise DecryptError(
f"Plain key must decode to 32 bytes for AES-GCM-256, got {len(plain_key)} bytes"
)
parsed = parse_segment_payload(encrypted)
try:
aesgcm = AESGCM(plain_key)
decrypted = aesgcm.decrypt(
parsed["iv"],
parsed["cipher"] + parsed["tag"],
parsed["aad"] if parsed["aad"] else None,
)
except InvalidTag as exc:
raise DecryptError(
"Decryption failed: authentication tag mismatch. "
"Possible causes: wrong key, corrupted encrypted content, or unsupported payload format."
) from exc
except Exception as exc:
raise DecryptError(
f"Decryption failed during AES-GCM operation: {type(exc).__name__}: {exc}"
) from exc
try:
plaintext = decrypted.decode("utf-8")
except UnicodeDecodeError as exc:
raise DecryptError(
"Decryption succeeded, but plaintext is not valid UTF-8 text. "
"The decrypted output may be binary data."
) from exc
return {
"_meta": {
"key_bytes": len(plain_key),
"encrypted_bytes": len(encrypted),
"iv_len": parsed["iv_len"],
"aad_len": parsed["aad_len"],
"cipher_len": parsed["cipher_len"],
"success": True,
},
"plaintext": plaintext,
"_debug": parsed,
}
def print_result(result: dict, debug: bool) -> None:
"""Print only the decrypted plain text unless debug is requested."""
print("Decrypted text:")
print(result["plaintext"])
if debug:
meta = result["_meta"]
parsed = result["_debug"]
print(file=sys.stderr)
print("Debug:", file=sys.stderr)
print(f" key_bytes = {meta['key_bytes']}", file=sys.stderr)
print(f" encrypted_bytes= {meta['encrypted_bytes']}", file=sys.stderr)
print(f" iv_len = {meta['iv_len']}", file=sys.stderr)
print(f" aad_len = {meta['aad_len']}", file=sys.stderr)
print(f" cipher_len = {meta['cipher_len']}", file=sys.stderr)
print(f" iv = {parsed['iv'].hex()}", file=sys.stderr)
print(
f" aad = {parsed['aad'].hex() if parsed['aad'] else '<empty>'}",
file=sys.stderr,
)
print(f" tag = {parsed['tag'].hex()}", file=sys.stderr)
def main() -> None:
"""Command-line entry point."""
parser = argparse.ArgumentParser(
description="Decrypt AES-GCM-256 segment-formatted text using Base64 key and Base64 payload."
)
parser.add_argument("--key", required=True, help="Base64-encoded 32-byte AES key")
parser.add_argument("--data", required=True, help="Base64-encoded encrypted payload")
parser.add_argument("--debug", action="store_true", help="Print payload parsing details")
args = parser.parse_args()
try:
result = decrypt_text(args.key, args.data)
print_result(result, args.debug)
sys.exit(0)
except DecryptError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
sys.exit(2)
except Exception as exc:
print(f"UNEXPECTED ERROR: {type(exc).__name__}: {exc}", file=sys.stderr)
sys.exit(3)
if __name__ == "__main__":
main()