Skip to content

Instantly share code, notes, and snippets.

@PyYoshi
Last active January 22, 2026 09:14
Show Gist options
  • Select an option

  • Save PyYoshi/39b3b1681f932dce358408df8fa855fa to your computer and use it in GitHub Desktop.

Select an option

Save PyYoshi/39b3b1681f932dce358408df8fa855fa to your computer and use it in GitHub Desktop.
jet_decrypt.py - Codeship Jet CLI Alternative

jet_decrypt.py - Codeship Jet CLI Alternative

A standalone Python script to decrypt files encrypted with Codeship's jet encrypt command, without requiring Docker.

Background

Codeship's jet CLI requires Docker to run, but recent Docker Engine API changes have broken compatibility. Additionally, Codeship is reaching end-of-life on January 31, 2026. This script provides a Docker-free alternative for decrypting your encrypted environment files.

Supported Format

  • ✅ jet v2 format (codeship:v2 header)
  • ✅ jet v1 format (legacy, no header)
  • Encryption: AES-256-CFB
  • Key: 32 bytes (base64 encoded, 44 characters)

Requirements

  • Python 3.9+
  • uv (recommended) or pip

Usage

With uv (recommended)

uv run jet_decrypt.py <encrypted_file> <output_file> <key_file>

# Example
uv run jet_decrypt.py secret.env.encrypted secret.env codeship.aes

With pip

pip install cryptography
python3 jet_decrypt.py secret.env.encrypted secret.env codeship.aes

File Format Details

[Encrypted file structure]
codeship:v2
<base64 encoded data>

[After base64 decode]
IV (16 bytes) + AES-256-CFB ciphertext

[After decryption]
MD5 checksum (32 hex chars) + original data

The script automatically strips the internal checksum from the decrypted output.

License

MIT


jet_decrypt.py - Codeship Jet CLI 代替ツール

Codeshipの jet encrypt で暗号化されたファイルを、Docker不要で復号化するPythonスクリプトです。

背景

Codeshipの jet CLIはDockerを必要としますが、最近のDocker Engine APIの変更により互換性が壊れ、動作しなくなるケースがあります。また、Codeshipは2026年1月31日にサービス終了予定です。このスクリプトはDocker不要で暗号化ファイルを復号化できる代替手段を提供します。

対応フォーマット

  • ✅ jet v2 形式(codeship:v2 ヘッダー付き)
  • ✅ jet v1 形式(レガシー、ヘッダーなし)
  • 暗号化方式: AES-256-CFB
  • 鍵: 32バイト(base64エンコード、44文字)

必要なもの

  • Python 3.9以上
  • uv(推奨)または pip

使い方

uv を使う場合(推奨)

uv run jet_decrypt.py <暗号化ファイル> <出力ファイル> <鍵ファイル>

#
uv run jet_decrypt.py secret.env.encrypted secret.env codeship.aes

pip を使う場合

pip install cryptography
python3 jet_decrypt.py secret.env.encrypted secret.env codeship.aes

ファイルフォーマット詳細

[暗号化ファイル構造]
codeship:v2
<base64エンコードされたデータ>

[base64デコード後]
IV (16バイト) + AES-256-CFB暗号文

[復号後]
MD5チェックサム (32文字hex) + 元データ

スクリプトは復号後のチェックサムを自動で除去します。

ライセンス

MIT

#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "cryptography",
# ]
# ///
"""
Codeship jet decrypt alternative - Docker不要の復号化ツール
Usage: uv run jet_decrypt.py <encrypted_file> <output_file> <key_file>
Example: uv run jet_decrypt.py secret.env.encrypted secret.env codeship.aes
"""
import sys
import base64
import hashlib
import hmac
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
V2_HEADER = "codeship:v2"
def main():
if len(sys.argv) < 4:
print("Usage: python3 jet_decrypt.py <encrypted_file> <output_file> <key_file>")
print("Example: python3 jet_decrypt.py secret.env.encrypted secret.env codeship.aes")
sys.exit(1)
encrypted_file = sys.argv[1]
output_file = sys.argv[2]
key_file = sys.argv[3]
# Read and decode key (base64 encoded 256-bit key)
with open(key_file, 'r') as f:
key_b64 = f.read().strip()
key = base64.b64decode(key_b64)
if len(key) != 32:
print(f"Invalid key length: expected 32 bytes, got {len(key)}")
sys.exit(1)
print(f"Key loaded: {len(key)} bytes")
# Read encrypted file
with open(encrypted_file, 'r') as f:
content = f.read()
# Check for v2 header
if content.startswith(V2_HEADER):
print("Detected v2 format")
payload = content[len(V2_HEADER):].strip()
decrypted = decrypt_v2(payload, key)
else:
print("No v2 header, trying v1 format...")
payload = content.strip()
decrypted = decrypt_v1(payload, key)
if decrypted is None:
print("All decryption methods failed!")
sys.exit(1)
# Write output
with open(output_file, 'wb') as f:
f.write(decrypted)
print(f"Successfully decrypted to: {output_file}")
print(f"Preview (first 200 chars): {decrypted[:200].decode('utf-8', errors='replace')}")
def decrypt_v2(payload: str, key: bytes) -> bytes:
"""Decrypt v2 format - tries multiple methods"""
try:
data = base64.b64decode(payload)
except Exception as e:
print(f"Base64 decode error: {e}")
return None
print(f"Decoded data length: {len(data)} bytes")
print(f"First 32 bytes (hex): {data[:32].hex()}")
methods = [
("AES-256-GCM (nonce=12)", lambda: decrypt_aes_gcm(data, key, nonce_size=12)),
("AES-256-GCM (nonce=16)", lambda: decrypt_aes_gcm(data, key, nonce_size=16)),
("AES-256-CBC+HMAC (hmac+iv+ct)", lambda: decrypt_aes_cbc_hmac_prefix(data, key)),
("AES-256-CBC+HMAC (iv+ct+hmac)", lambda: decrypt_aes_cbc_hmac_suffix(data, key)),
("AES-256-CBC (iv+ct)", lambda: decrypt_aes_cbc(data, key)),
("AES-256-CFB (iv+ct)", lambda: decrypt_aes_cfb(data, key)),
("AES-256-CTR (iv+ct)", lambda: decrypt_aes_ctr(data, key)),
]
for name, method in methods:
try:
result = method()
if result and is_printable(result):
print(f"Success with: {name}")
# v2 format: first 32 chars are checksum (MD5 hex), remove it
result = strip_v2_checksum(result)
return result
except Exception as e:
print(f"{name} failed: {e}")
return None
def strip_v2_checksum(data: bytes) -> bytes:
"""Remove v2 checksum (32 hex chars) from start of decrypted data"""
try:
text = data.decode('utf-8')
# Check if first 32 chars look like hex checksum
if len(text) >= 32:
potential_checksum = text[:32]
if all(c in '0123456789abcdef' for c in potential_checksum.lower()):
print(f"Stripped v2 checksum: {potential_checksum}")
# Also strip newline after checksum if present
remaining = text[32:]
if remaining.startswith('\n'):
remaining = remaining[1:]
return remaining.encode('utf-8')
return data
except UnicodeDecodeError:
return data
def decrypt_v1(payload: str, key: bytes) -> bytes:
"""Decrypt v1 format"""
try:
data = base64.b64decode(payload)
except Exception as e:
print(f"Base64 decode error: {e}")
return None
methods = [
("AES-256-CBC", lambda: decrypt_aes_cbc(data, key)),
("AES-256-CFB", lambda: decrypt_aes_cfb(data, key)),
("AES-256-CTR", lambda: decrypt_aes_ctr(data, key)),
]
for name, method in methods:
try:
result = method()
if result and is_printable(result):
print(f"Success with: {name}")
return result
except Exception as e:
print(f"{name} failed: {e}")
return None
def decrypt_aes_gcm(data: bytes, key: bytes, nonce_size: int = 12) -> bytes:
"""AES-256-GCM decryption"""
if len(data) < nonce_size + 16: # nonce + min tag
raise ValueError("Data too short for GCM")
nonce = data[:nonce_size]
ciphertext_and_tag = data[nonce_size:]
# GCM tag is typically last 16 bytes
cipher = Cipher(algorithms.AES(key), modes.GCM(nonce, ciphertext_and_tag[-16:]), backend=default_backend())
decryptor = cipher.decryptor()
plaintext = decryptor.update(ciphertext_and_tag[:-16]) + decryptor.finalize()
return plaintext
def decrypt_aes_cbc_hmac_prefix(data: bytes, key: bytes) -> bytes:
"""AES-256-CBC with HMAC at start: hmac(32) + iv(16) + ciphertext"""
if len(data) < 48:
raise ValueError("Data too short")
expected_mac = data[:32]
iv = data[32:48]
ciphertext = data[48:]
# Verify HMAC
mac = hmac.new(key, data[32:], hashlib.sha256)
if not hmac.compare_digest(expected_mac, mac.digest()):
raise ValueError("HMAC verification failed")
return decrypt_aes_cbc_core(ciphertext, key, iv)
def decrypt_aes_cbc_hmac_suffix(data: bytes, key: bytes) -> bytes:
"""AES-256-CBC with HMAC at end: iv(16) + ciphertext + hmac(32)"""
if len(data) < 48:
raise ValueError("Data too short")
expected_mac = data[-32:]
iv = data[:16]
ciphertext = data[16:-32]
# Verify HMAC
mac = hmac.new(key, data[:-32], hashlib.sha256)
if not hmac.compare_digest(expected_mac, mac.digest()):
raise ValueError("HMAC verification failed")
return decrypt_aes_cbc_core(ciphertext, key, iv)
def decrypt_aes_cbc(data: bytes, key: bytes) -> bytes:
"""AES-256-CBC: iv(16) + ciphertext"""
if len(data) < 32:
raise ValueError("Data too short")
iv = data[:16]
ciphertext = data[16:]
return decrypt_aes_cbc_core(ciphertext, key, iv)
def decrypt_aes_cbc_core(ciphertext: bytes, key: bytes, iv: bytes) -> bytes:
"""Core AES-CBC decryption with PKCS7 unpadding"""
if len(ciphertext) % 16 != 0:
raise ValueError("Ciphertext not multiple of block size")
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
# PKCS7 unpad
padding_len = plaintext[-1]
if padding_len > 16 or padding_len == 0:
raise ValueError("Invalid PKCS7 padding")
for i in range(padding_len):
if plaintext[-(i+1)] != padding_len:
raise ValueError("Invalid PKCS7 padding bytes")
return plaintext[:-padding_len]
def decrypt_aes_cfb(data: bytes, key: bytes) -> bytes:
"""AES-256-CFB: iv(16) + ciphertext"""
if len(data) < 16:
raise ValueError("Data too short")
iv = data[:16]
ciphertext = data[16:]
cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend=default_backend())
decryptor = cipher.decryptor()
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
return plaintext
def decrypt_aes_ctr(data: bytes, key: bytes) -> bytes:
"""AES-256-CTR: nonce(16) + ciphertext"""
if len(data) < 16:
raise ValueError("Data too short")
nonce = data[:16]
ciphertext = data[16:]
cipher = Cipher(algorithms.AES(key), modes.CTR(nonce), backend=default_backend())
decryptor = cipher.decryptor()
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
return plaintext
def is_printable(data: bytes) -> bool:
"""Check if decrypted data looks like valid text"""
try:
text = data.decode('utf-8')
# Check if mostly printable ASCII or valid UTF-8
printable_count = sum(1 for c in text if c.isprintable() or c in '\n\r\t')
return printable_count > len(text) * 0.8
except UnicodeDecodeError:
return False
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment