Skip to content

jenkins-decryptor

#!/usr/bin/env python3
import re
import sys
import base64
from hashlib import sha256
from Crypto.Cipher import AES
from pathlib import Path

MAGIC = b"::::MAGIC::::"

def load_secret(master_key_path: Path, hudson_secret_path: Path) -> bytes:
    master_key = master_key_path.read_bytes()
    hashed_master = sha256(master_key).digest()[:16]

    hudson_secret = hudson_secret_path.read_bytes()
    ecb = AES.new(hashed_master, AES.MODE_ECB)
    decrypted = ecb.decrypt(hudson_secret)

    secret = decrypted[:-16][:16]
    return secret

def pkcs7_unpad(blocks: bytes) -> bytes:
    pad_len = blocks[-1]
    if pad_len <= 16:
        return blocks[:-pad_len]
    return blocks 

def decrypt_new(secret: bytes, payload: bytes) -> str:
    p = payload[1:]
    iv_len = (p[0]<<24) | (p[1]<<16) | (p[2]<<8) | p[3]; p = p[4:]
    data_len = (p[0]<<24) | (p[1]<<16) | (p[2]<<8) | p[3]; p = p[4:]

    iv = p[:iv_len]; cipher = p[iv_len:]
    aes = AES.new(secret, AES.MODE_CBC, iv)
    decrypted = aes.decrypt(cipher)

    plaintext = pkcs7_unpad(decrypted)
    return plaintext.decode("utf-8", errors="replace")

def decrypt_old(secret: bytes, payload: bytes) -> str:
    aes = AES.new(secret, AES.MODE_ECB)
    decrypted = aes.decrypt(payload)
    return decrypted.split(MAGIC)[0].decode("utf-8", errors="replace")

def find_encrypted_strings(xml_text: str) -> list[str]:
    pattern = re.compile(
        r"<(?:password|passphrase|privateKey)>\s*\{(.*?)\}\s*</(?:password|passphrase|privateKey)>",
        re.DOTALL
    )
    return pattern.findall(xml_text)

def main():
    if len(sys.argv) != 4:
        print(f"Usage: {Path(sys.argv[0]).name} <master.key> <hudson.util.Secret> <credentials.xml>")
        sys.exit(1)

    secret = load_secret(Path(sys.argv[1]), Path(sys.argv[2]))
    xml_text = Path(sys.argv[3]).read_text(encoding="utf-8", errors="ignore")

    for b64 in find_encrypted_strings(xml_text):
        payload = base64.b64decode(b64)
        if payload and payload[0] == 1:
            print(decrypt_new(secret, payload))
        else:
            print(decrypt_old(secret, payload))

if __name__ == "__main__":
    main()