Skip to content

Instantly share code, notes, and snippets.

@theamanbhargava
Created April 16, 2026 13:27
Show Gist options
  • Select an option

  • Save theamanbhargava/bded50e43ceec109925198aba9508b96 to your computer and use it in GitHub Desktop.

Select an option

Save theamanbhargava/bded50e43ceec109925198aba9508b96 to your computer and use it in GitHub Desktop.
Unofficial Python decryptor for Income Tax AIS Utility JSON exports (PBKDF2/AES-CBC)

AIS Utility JSON Decryptor

Unofficial Python script to decrypt the Income Tax Department's encrypted AIS JSON files downloaded for the AIS Utility.

This script currently handles the observed file format:

  • first 32 hex characters: IV
  • next 32 hex characters: salt
  • remaining data: ciphertext in Base64 or hex
  • key derivation: PBKDF2-HMAC-SHA256, 1000 iterations, 32-byte key
  • cipher mode: AES-CBC with PKCS#7 padding

Install

python3 -m pip install -r requirements.txt

Usage

Auto-try known password constructions:

python3 decrypt_ais.py ENCRYPTED_AIS.json ABCDE1234F 01011990

Pass an explicit middle segment:

python3 decrypt_ais.py ENCRYPTED_AIS.json ABCDE1234F 01011990 --password-middle 'VALUE'

Pass the exact password directly:

python3 decrypt_ais.py ENCRYPTED_AIS.json ABCDE1234F 01011990 --password 'FULL_PASSWORD'

The decrypted JSON is written next to the input file as *_decrypted.json.

Notes

  • This is not an official Income Tax Department utility.
  • Password construction details may change across AIS utility versions.
  • If auto-detection fails, use --password or --password-middle.
  • Do not publish or share your decrypted AIS output.
#!/usr/bin/env python3
"""
Decrypt AIS (Annual Information Statement) JSON files
downloaded from the Indian Income Tax portal.
Usage:
python3 decrypt_ais.py <encrypted_file> <PAN> <DOB>
python3 decrypt_ais.py <encrypted_file> <PAN> <DOB> [--password-middle VALUE]
python3 decrypt_ais.py <encrypted_file> <PAN> <DOB> --password VALUE
PAN - Your PAN number (e.g. ABCPD1234E)
DOB - Date of birth in ddmmyyyy format (e.g. 15061990)
"""
import argparse
import sys
import json
import base64
from pathlib import Path
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Util.Padding import unpad
PASSWORD_MIDDLE = "GQ39%*g"
def _load_encrypted_payload(filepath):
raw = Path(filepath).read_text(encoding="utf-8").strip()
# Some tools export the encrypted AIS string as a JSON string instead of raw text.
if raw.startswith('"') and raw.endswith('"'):
raw = json.loads(raw)
if len(raw) < 64:
raise ValueError(
"Encrypted AIS file is too short. Expected IV(32 hex) + salt(32 hex) + ciphertext."
)
iv_hex = raw[:32]
salt_hex = raw[32:64]
ciphertext_part = raw[64:]
try:
iv = bytes.fromhex(iv_hex)
salt = bytes.fromhex(salt_hex)
except ValueError as exc:
raise ValueError(
"The first 64 characters of the AIS file must be hexadecimal IV + salt."
) from exc
try:
ciphertext = base64.b64decode(ciphertext_part, validate=True)
except Exception:
try:
ciphertext = bytes.fromhex(ciphertext_part)
except ValueError as exc:
raise ValueError(
"Ciphertext must be valid Base64 or hexadecimal data."
) from exc
if not ciphertext:
raise ValueError("Ciphertext payload is empty.")
return iv, salt, ciphertext
def _password_candidates(pan, dob, password_middle=None):
lower_pan = pan.lower()
upper_pan = pan.upper()
candidates = []
seen = set()
def add(candidate):
if candidate not in seen:
seen.add(candidate)
candidates.append(candidate)
if password_middle is not None:
add(f"{lower_pan}{password_middle}{dob}")
return candidates
# Try the currently observed AIS utility format first, then simpler variants.
add(f"{lower_pan}{PASSWORD_MIDDLE}{dob}")
add(f"{lower_pan}{dob}")
add(f"{upper_pan}{dob}")
return candidates
def _decrypt_with_password(iv, salt, ciphertext, password):
key = PBKDF2(
password.encode("utf-8"),
salt,
dkLen=32,
count=1000,
hmac_hash_module=SHA256,
)
cipher = AES.new(key, AES.MODE_CBC, iv)
return unpad(cipher.decrypt(ciphertext), AES.block_size).decode("utf-8")
def decrypt_ais(filepath, pan, dob, password_middle=None, password=None):
iv, salt, ciphertext = _load_encrypted_payload(filepath)
last_error = None
plaintext = None
if password is not None:
password_candidates = [password]
else:
password_candidates = _password_candidates(
pan, dob, password_middle=password_middle
)
for candidate in password_candidates:
try:
plaintext = _decrypt_with_password(iv, salt, ciphertext, candidate)
break
except Exception as exc:
last_error = exc
if plaintext is None:
raise ValueError(
"Decryption failed. Check PAN, DOB, and that the AIS file is the encrypted utility JSON. "
"If needed, pass --password-middle explicitly."
) from last_error
try:
data = json.loads(plaintext)
except json.JSONDecodeError as exc:
raise ValueError("Decryption completed but the result was not valid JSON.") from exc
in_path = Path(filepath)
out_path = in_path.with_name(f"{in_path.stem}_decrypted.json")
with out_path.open("w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f"Decrypted successfully -> {out_path}")
return str(out_path)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("filepath", help="Path to the encrypted AIS file")
parser.add_argument("pan", help="PAN number")
parser.add_argument("dob", help="Date of birth in ddmmyyyy format")
parser.add_argument(
"--password-middle",
default=None,
help="Optional string inserted between pan.lower() and dob before key derivation",
)
parser.add_argument(
"--password",
default=None,
help="Optional full password to use directly for PBKDF2, bypassing password construction",
)
args = parser.parse_args()
decrypt_ais(
args.filepath,
args.pan,
args.dob,
password_middle=args.password_middle,
password=args.password,
)
pycryptodome>=3.20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment