aboutsummaryrefslogtreecommitdiff
path: root/profanity-aesgcm-handler
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukasz.kasprzak@pm.me>2026-04-13 17:37:26 +0200
committerLukasz Kasprzak <lukasz.kasprzak@pm.me>2026-04-13 17:37:26 +0200
commit51d43498b07dc97d795947964534f0903cd05db5 (patch)
tree735712bd50b5c244ef922a3b873c709ecce604cd /profanity-aesgcm-handler
parent39711cf6c2ec5a3b4480dcb4800cc3802bda5bf2 (diff)
downloadbin-51d43498b07dc97d795947964534f0903cd05db5.tar.gz
bin-51d43498b07dc97d795947964534f0903cd05db5.zip
routine backup
Diffstat (limited to 'profanity-aesgcm-handler')
-rwxr-xr-xprofanity-aesgcm-handler113
1 files changed, 113 insertions, 0 deletions
diff --git a/profanity-aesgcm-handler b/profanity-aesgcm-handler
new file mode 100755
index 0000000..0ea5e19
--- /dev/null
+++ b/profanity-aesgcm-handler
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+
+import os
+import re
+import sys
+import urllib.parse
+from pathlib import Path
+
+import requests
+from cryptography.hazmat.primitives.ciphers.aead import AESGCM
+
+
+home = Path.home()
+if (home / "downloads").exists():
+ DOWNLOAD_DIR = home / "downloads" / "xmpp"
+ DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
+else:
+ DOWNLOAD_DIR = home / "Downloads" / "xmpp"
+ DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
+
+def log(msg: str) -> None:
+ log_file = Path.home() / ".local" / "share" / "profanity-aesgcm" / "handler.log"
+ log_file.parent.mkdir(parents=True, exist_ok=True)
+ with log_file.open("a", encoding="utf-8") as f:
+ f.write(msg + "\n")
+
+
+def extract_uri(message: str) -> str | None:
+ m = re.search(r'(aesgcm://\S+)', message)
+ return m.group(1) if m else None
+
+
+def parse_aesgcm_uri(uri: str):
+ parsed = urllib.parse.urlparse(uri)
+
+ if parsed.scheme != "aesgcm":
+ raise ValueError("Unsupported scheme")
+
+ fragment = parsed.fragment.strip()
+ frag_hex = "".join(fragment.split()) # remove accidental spaces/newlines
+
+ # XEP-0454 style: 12-byte IV (24 hex chars) + 32-byte key (64 hex chars)
+ if len(frag_hex) < 88:
+ raise ValueError(f"Fragment too short: {len(frag_hex)} hex chars")
+
+ iv_hex = frag_hex[:24]
+ key_hex = frag_hex[24:88]
+
+ iv = bytes.fromhex(iv_hex)
+ key = bytes.fromhex(key_hex)
+
+ https_url = urllib.parse.urlunparse(("https", parsed.netloc, parsed.path, "", "", ""))
+ filename = os.path.basename(parsed.path) or "download.bin"
+
+ return https_url, filename, key, iv
+
+
+def download_file(url: str) -> bytes:
+ r = requests.get(url, timeout=60)
+ r.raise_for_status()
+ return r.content
+
+
+def decrypt_xmpp_aesgcm(data: bytes, key: bytes, iv: bytes) -> bytes:
+ # AESGCM.decrypt expects ciphertext || tag
+ aesgcm = AESGCM(key)
+ return aesgcm.decrypt(iv, data, None)
+
+
+def unique_path(path: Path) -> Path:
+ if not path.exists():
+ return path
+
+ stem = path.stem
+ suffix = path.suffix
+
+ for i in range(1, 1000):
+ candidate = path.with_name(f"{stem}-{i}{suffix}")
+ if not candidate.exists():
+ return candidate
+
+ raise RuntimeError("Could not create unique filename")
+
+def main():
+ if len(sys.argv) < 2:
+ sys.exit(0)
+
+ message = sys.argv[1]
+ uri = extract_uri(message)
+ if not uri:
+ sys.exit(0)
+
+ try:
+ https_url, filename, key, iv = parse_aesgcm_uri(uri)
+ enc = download_file(https_url)
+ plain = decrypt_xmpp_aesgcm(enc, key, iv)
+
+ out_path = unique_path(DOWNLOAD_DIR / filename)
+ out_path.write_bytes(plain)
+
+ log(f"OK saved: {out_path}")
+ print(str(out_path))
+
+ # Optional: uncomment if you want auto-open on desktop
+ # os.system(f'xdg-open "{out_path}" >/dev/null 2>&1 &')
+
+ except Exception as e:
+ log(f"ERROR: {e}")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()