feat(kik): Implement document ID encryption for KİK v2 API

Reverse engineered the AES-256-CBC encryption used by KİK's Angular web
application to generate document URL hashes from numeric IDs.

Key findings:
- Algorithm: AES-256-CBC with PKCS7 padding
- Key location: ekapv2.kik.gov.tr module 21554 (environment config)
- Output format: IV (16 bytes hex) + Ciphertext (16 bytes hex) = 64 chars

Changes:
- Added encrypt_document_id() static method to KikV2ApiClient
- Updated get_document_markdown() to auto-encrypt numeric gundemMaddesiId
- Added cryptography>=44.0.0 dependency for AES encryption
- Both primary and fallback URL paths now support encryption

This enables direct document retrieval from numeric search result IDs
without requiring the pre-encrypted hash from the web interface.
This commit is contained in:
saidsurucu
2025-12-04 15:17:13 +03:00
parent 91ad04cf09
commit a2b50951e9
3 changed files with 84 additions and 6 deletions
+78 -3
View File
@@ -8,9 +8,18 @@ import base64
import ssl import ssl
import subprocess import subprocess
import shutil import shutil
import os
from typing import Optional from typing import Optional
from datetime import datetime from datetime import datetime
# Cryptography imports for AES-256-CBC encryption of document IDs
try:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
HAS_CRYPTOGRAPHY = True
except ImportError:
HAS_CRYPTOGRAPHY = False
from .models_v2 import ( from .models_v2 import (
KikV2DecisionType, KikV2SearchPayload, KikV2SearchPayloadDk, KikV2SearchPayloadMk, KikV2DecisionType, KikV2SearchPayload, KikV2SearchPayloadDk, KikV2SearchPayloadMk,
KikV2RequestData, KikV2QueryRequest, KikV2KeyValuePair, KikV2RequestData, KikV2QueryRequest, KikV2KeyValuePair,
@@ -37,6 +46,54 @@ class KikV2ApiClient:
KikV2DecisionType.MAHKEME: "/b_ihalearaclari/api/KurulKararlari/GetKurulKararlariMk" KikV2DecisionType.MAHKEME: "/b_ihalearaclari/api/KurulKararlari/GetKurulKararlariMk"
} }
# AES-256-CBC encryption key for document ID encryption (reverse engineered from ekapv2.kik.gov.tr Angular app)
# This key is used to encrypt numeric gundemMaddesiId values to 64-character hex hashes for document URLs
DOCUMENT_ID_ENCRYPTION_KEY = bytes([
236, 193, 164, 43, 12, 135, 121, 170, 4, 244, 123, 219, 82, 158, 124, 174,
174, 228, 219, 174, 208, 104, 174, 120, 32, 76, 250, 4, 143, 159, 211, 176
])
@staticmethod
def encrypt_document_id(numeric_id: str) -> str:
"""
Encrypt a numeric KİK gundemMaddesiId to the 64-character hex hash
used in document URLs.
Algorithm: AES-256-CBC with PKCS7 padding
Output format: IV (16 bytes hex) + Ciphertext (16 bytes hex) = 64 chars
Args:
numeric_id: The numeric document ID from search results (e.g., "177280")
Returns:
64-character hex string for use in document URL KararId parameter
"""
if not HAS_CRYPTOGRAPHY:
raise ImportError("cryptography library required for document ID encryption")
# Generate random IV (16 bytes)
iv = os.urandom(16)
# Create AES-CBC cipher with the encryption key
cipher = Cipher(
algorithms.AES(KikV2ApiClient.DOCUMENT_ID_ENCRYPTION_KEY),
modes.CBC(iv),
backend=default_backend()
)
encryptor = cipher.encryptor()
# Encode plaintext and apply PKCS7 padding
plaintext = numeric_id.encode('utf-8')
block_size = 16
padding_len = block_size - (len(plaintext) % block_size)
padded_plaintext = plaintext + bytes([padding_len] * padding_len)
# Encrypt
ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize()
# Return IV + ciphertext as lowercase hex (64 characters total)
return iv.hex() + ciphertext.hex()
def __init__(self, request_timeout: float = 60.0): def __init__(self, request_timeout: float = 60.0):
# Create SSL context with legacy server support # Create SSL context with legacy server support
ssl_context = ssl.create_default_context() ssl_context = ssl.create_default_context()
@@ -322,14 +379,32 @@ class KikV2ApiClient:
error_message="Could not get document URL from GetSorgulamaUrl API" error_message="Could not get document URL from GetSorgulamaUrl API"
) )
# Construct full document URL with the actual document ID # If document_id is numeric, encrypt it to get the KararId hash
document_url = f"{base_document_url}?KararId={document_id}" # The web interface uses AES-256-CBC encrypted hashes for document URLs
karar_id = document_id
if document_id.isdigit():
try:
karar_id = self.encrypt_document_id(document_id)
logger.info(f"KikV2ApiClient: Encrypted numeric ID {document_id} to hash: {karar_id}")
except Exception as enc_error:
logger.warning(f"KikV2ApiClient: Could not encrypt document ID, using as-is: {enc_error}")
# Construct full document URL with the encrypted KararId
document_url = f"{base_document_url}?KararId={karar_id}"
logger.info(f"KikV2ApiClient: Step 2 - Retrieved document URL: {document_url}") logger.info(f"KikV2ApiClient: Step 2 - Retrieved document URL: {document_url}")
except Exception as e: except Exception as e:
logger.error(f"KikV2ApiClient: Error getting document URL for ID {document_id}: {str(e)}") logger.error(f"KikV2ApiClient: Error getting document URL for ID {document_id}: {str(e)}")
# Fallback to old method if GetSorgulamaUrl fails # Fallback to old method if GetSorgulamaUrl fails
document_url = f"https://ekap.kik.gov.tr/EKAP/Vatandas/KurulKararGoster.aspx?KararId={document_id}" # Also encrypt numeric IDs in fallback path
karar_id = document_id
if document_id.isdigit():
try:
karar_id = self.encrypt_document_id(document_id)
logger.info(f"KikV2ApiClient: Encrypted numeric ID in fallback: {karar_id}")
except Exception as enc_error:
logger.warning(f"KikV2ApiClient: Could not encrypt in fallback: {enc_error}")
document_url = f"https://ekap.kik.gov.tr/EKAP/Vatandas/KurulKararGoster.aspx?KararId={karar_id}"
logger.info(f"KikV2ApiClient: Falling back to direct URL: {document_url}") logger.info(f"KikV2ApiClient: Falling back to direct URL: {document_url}")
try: try:
+1
View File
@@ -29,6 +29,7 @@ dependencies = [
"fastmcp>=2.10.5", "fastmcp>=2.10.5",
"pypdf>=5.5.0", "pypdf>=5.5.0",
"fastapi>=0.115.14", "fastapi>=0.115.14",
"cryptography>=44.0.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]
Generated
+2
View File
@@ -2333,6 +2333,7 @@ source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiohttp" }, { name = "aiohttp" },
{ name = "beautifulsoup4" }, { name = "beautifulsoup4" },
{ name = "cryptography" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "fastmcp" }, { name = "fastmcp" },
{ name = "httpx" }, { name = "httpx" },
@@ -2368,6 +2369,7 @@ requires-dist = [
{ name = "aiohttp", specifier = ">=3.11.18" }, { name = "aiohttp", specifier = ">=3.11.18" },
{ name = "beautifulsoup4", specifier = ">=4.13.4" }, { name = "beautifulsoup4", specifier = ">=4.13.4" },
{ name = "clerk-backend-api", marker = "extra == 'saas'", specifier = ">=3.0.0" }, { name = "clerk-backend-api", marker = "extra == 'saas'", specifier = ">=3.0.0" },
{ name = "cryptography", specifier = ">=44.0.0" },
{ name = "fastapi", specifier = ">=0.115.14" }, { name = "fastapi", specifier = ">=0.115.14" },
{ name = "fastapi", marker = "extra == 'api'", specifier = ">=0.115.0" }, { name = "fastapi", marker = "extra == 'api'", specifier = ">=0.115.0" },
{ name = "fastmcp", specifier = ">=2.10.5" }, { name = "fastmcp", specifier = ">=2.10.5" },