|
""" |
|
Keep enkzip decrypter. |
|
|
|
Algorithm reverse-engineered from libenigmaa.so (Java_com_gotokeep_cryp_Enigma_decrypt |
|
→ FUN_0011ae78 → FUN_0011f678 → FUN_001205e8/FUN_00120dac): |
|
|
|
1. The "Bearer <jwt>" token is parsed as a JWT; the payload's "_id" field is |
|
used as user_id (24-char Mongo ObjectId hex). |
|
2. The bytes hashed are user_id rotated left by 13: |
|
hash_input = user_id[13:] || user_id[:13] |
|
(decompiled MD5 routine writes user_id[13:] into the buffer, then patches |
|
in user_id[0:8] and user_id[5:13] on top — the net effect is the rotation.) |
|
3. AES-256 key = lowercase hex(MD5(hash_input)).encode("ascii") (32 bytes). |
|
4. enkzip is "<nonce_b64url>.<ciphertext+tag_b64url>". |
|
- nonce_b64url decodes to 12 bytes (AES-GCM IV). |
|
- ciphertext+tag_b64url decodes to ciphertext || tag(16) (AES-256-GCM). |
|
5. Standard AES-256-GCM with empty AAD; the tag verifies. |
|
""" |
|
|
|
import base64 |
|
import hashlib |
|
import json |
|
import sys |
|
|
|
from Crypto.Cipher import AES |
|
|
|
|
|
def b64url(s: str) -> bytes: |
|
return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)) |
|
|
|
|
|
def jwt_payload(token: str) -> dict: |
|
return json.loads(b64url(token.split(".")[1])) |
|
|
|
|
|
def derive_key(user_id: str) -> bytes: |
|
rotated = user_id[13:] + user_id[:13] |
|
return hashlib.md5(rotated.encode()).hexdigest().encode("ascii") |
|
|
|
|
|
def decrypt_enkzip(enkzip: str, key: bytes) -> bytes: |
|
nonce_part, body_part = enkzip.split(".", 1) |
|
nonce = b64url(nonce_part) |
|
blob = b64url(body_part) |
|
ct, tag = blob[:-16], blob[-16:] |
|
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) |
|
return cipher.decrypt_and_verify(ct, tag) |
|
|
|
|
|
def encrypt_enkzip(plaintext: bytes, key: bytes, nonce: bytes) -> str: |
|
assert len(nonce) == 12 |
|
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) |
|
ct, tag = cipher.encrypt_and_digest(plaintext) |
|
enc_nonce = base64.urlsafe_b64encode(nonce).rstrip(b"=").decode() |
|
enc_body = base64.urlsafe_b64encode(ct + tag).rstrip(b"=").decode() |
|
return f"{enc_nonce}.{enc_body}" |
|
|
|
|
|
def main(): |
|
|
|
""" |
|
token is jwt token located in Authorization header without `Bearer ` prefix |
|
""" |
|
token = "aaa.bbb.ccc" |
|
enkzip = "ddd.eee" |
|
|
|
|
|
user_id = jwt_payload(token)["_id"] |
|
key = derive_key(user_id) |
|
|
|
print(f"user_id : {user_id}") |
|
print(f"hash input : {user_id[13:] + user_id[:13]}") |
|
print(f"AES-256 key : {key.decode()} (md5 hex of rotated user_id, ASCII)") |
|
|
|
print(f"enkzip : {len(enkzip)} chars ({enkzip[:40]}...)") |
|
pt = decrypt_enkzip(enkzip, key) |
|
print(f"plaintext : {len(pt)} bytes") |
|
|
|
out_path = "res_decrypted.json" |
|
with open(out_path, "wb") as f: |
|
f.write(pt) |
|
print(f"wrote {out_path}") |
|
|
|
# Round-trip sanity check (pick the same nonce so we get the same enkzip). |
|
nonce_part, _ = enkzip.split(".", 1) |
|
nonce = b64url(nonce_part) |
|
re_enc = encrypt_enkzip(pt, key, nonce) |
|
assert re_enc == enkzip, "encrypt round-trip mismatch" |
|
print("round-trip : OK (re-encrypt produces identical enkzip)") |
|
|
|
head = json.loads(pt) |
|
keys = list(head.keys()) if isinstance(head, dict) else [] |
|
print(f"top-level keys: {keys[:10]}{'...' if len(keys) > 10 else ''}") |
|
|
|
|
|
if __name__ == "__main__": |
|
sys.exit(main()) |