Created
July 26, 2026 18:12
-
-
Save Pldare/cca34b4d08fcb0bb33860972f965eb55 to your computer and use it in GitHub Desktop.
tokimeki-idol tool
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Decrypt DB/*.db.bytes files (SQLCipher-encrypted) to plaintext SQLite databases. | |
| Reverse-engineered from libil2cpp.so (IL2CPP) via IDA Pro analysis. | |
| === Decryption Algorithm === | |
| Encryption: SQLCipher 3.x format | |
| - AES-256-CBC | |
| - PBKDF2-HMAC-SHA1 key derivation | |
| - 64000 KDF iterations | |
| - Page size: 1024 bytes | |
| - Reserve: 48 bytes per page (16 IV + 20 HMAC + 12 padding) | |
| - Password passed to sqlite3_key() | |
| === Password Derivation === | |
| Key function: ProjectIP.Auy.WTDataController<object>.CreateSQLiteConnection | |
| Address: 0x1A04550 in libil2cpp.so | |
| Logic (C# pseudocode): | |
| string str2 = tableName + ".db"; | |
| string password; | |
| int openFlags; | |
| if (tableName.StartsWith("ms_")) | |
| { | |
| password = "password"; // hardcoded literal | |
| openFlags = 196609; // READWRITE | CREATE | FULLMUTEX | |
| } | |
| else | |
| { | |
| string hashCode = ExtensionMethods.GetHashCode(str2); | |
| string combined = "t0k1d0l4ever" + hashCode; | |
| password = ExtensionMethods.GetHashCode(combined); | |
| openFlags = 196614; // READWRITE | CREATE | FULLMUTEX | SHAREDCACHE | |
| } | |
| ExtensionMethods.GetHashCode (Address: 0x186E2EC): | |
| 1. UTF-8 encode input string | |
| 2. SHA1 hash | |
| 3. BitConverter.ToString(hash) -> "XX-XX-XX-..." (uppercase hex, dash-separated) | |
| 4. Replace("-", "") -> 40-char uppercase hex string | |
| === Verified Result === | |
| All 38 DB files in the DB/ directory start with "ms_", so the password is | |
| the literal string "password" for every file. | |
| Usage: | |
| python decrypt_db.py # Decrypt all DB/*.db.bytes | |
| python decrypt_db.py <file> <output> # Decrypt a single file | |
| python decrypt_db.py list # List passwords for all files | |
| """ | |
| import hashlib | |
| import os | |
| import struct | |
| import sys | |
| from Crypto.Cipher import AES | |
| # --- Password derivation --- | |
| def get_hash_code(s: str) -> str: | |
| """Replicate ExtensionMethods.GetHashCode: SHA1 -> uppercase hex (no dashes).""" | |
| b = s.encode("utf-8") | |
| h = hashlib.sha1(b).digest() | |
| hexstr = "-".join("{:02X}".format(x) for x in h) | |
| return hexstr.replace("-", "") | |
| def derive_password(db_name_no_ext: str) -> str: | |
| """Derive the SQLCipher password for a given DB table name.""" | |
| str2 = db_name_no_ext + ".db" | |
| if db_name_no_ext.startswith("ms_"): | |
| return "password" | |
| else: | |
| h1 = get_hash_code(str2) | |
| combined = "t0k1d0l4ever" + h1 | |
| return get_hash_code(combined) | |
| # --- SQLCipher decryption --- | |
| def pbkdf2_hmac_sha1(password, salt, iterations, dklen): | |
| return hashlib.pbkdf2_hmac('sha1', password, salt, iterations, dklen) | |
| def decrypt_sqlcipher_db(db_bytes, password, page_size=1024, kdf_iter=64000, reserve=48): | |
| """Decrypt a SQLCipher 3.x database to plaintext SQLite format.""" | |
| salt = db_bytes[:16] | |
| key = pbkdf2_hmac_sha1(password.encode(), salt, kdf_iter, 32) | |
| num_pages = len(db_bytes) // page_size | |
| output = bytearray() | |
| for i in range(num_pages): | |
| page_start = i * page_size | |
| page = db_bytes[page_start:page_start + page_size] | |
| is_page1 = (i == 0) | |
| iv_offset = page_size - reserve | |
| iv = page[iv_offset:iv_offset + 16] | |
| if is_page1: | |
| ciphertext = page[16:iv_offset] | |
| else: | |
| ciphertext = page[0:iv_offset] | |
| cipher = AES.new(key, AES.MODE_CBC, iv) | |
| decrypted = cipher.decrypt(ciphertext) | |
| if is_page1: | |
| output.extend(b'SQLite format 3\x00') | |
| output.extend(decrypted) | |
| current_len = 16 + len(decrypted) | |
| if current_len < page_size: | |
| output.extend(b'\x00' * (page_size - current_len)) | |
| else: | |
| output.extend(decrypted) | |
| if len(decrypted) < page_size: | |
| output.extend(b'\x00' * (page_size - len(decrypted))) | |
| # Set reserved-space field (header offset 20) to 0 for plaintext SQLite | |
| output[20] = 0 | |
| return bytes(output) | |
| def verify_decryption(decrypted_bytes): | |
| """Check if decrypted bytes start with a valid SQLite header.""" | |
| magic = decrypted_bytes[:16] | |
| if magic != b'SQLite format 3\x00': | |
| return False, "Invalid SQLite header magic" | |
| page_size = struct.unpack('>H', decrypted_bytes[16:18])[0] | |
| if page_size == 1: | |
| page_size = 65536 | |
| if page_size not in (512, 1024, 2048, 4096, 8192, 16384, 32768, 65536): | |
| return False, f"Invalid page size: {page_size}" | |
| return True, f"Valid SQLite header, page size: {page_size}" | |
| def decrypt_file(db_path, output_path, password=None): | |
| """Decrypt a single .db.bytes file to a plaintext .db file.""" | |
| with open(db_path, 'rb') as f: | |
| data = f.read() | |
| name = os.path.basename(db_path) | |
| if name.endswith('.db.bytes'): | |
| base_name = name[:-len('.db.bytes')] | |
| elif name.endswith('.db'): | |
| base_name = name[:-len('.db')] | |
| else: | |
| base_name = name | |
| if password is None: | |
| password = derive_password(base_name) | |
| decrypted = decrypt_sqlcipher_db(data, password) | |
| valid, msg = verify_decryption(decrypted) | |
| with open(output_path, 'wb') as f: | |
| f.write(decrypted) | |
| return password, valid, msg | |
| def list_db_files(db_dir): | |
| """List all .db.bytes files and their derived passwords.""" | |
| files = sorted(f for f in os.listdir(db_dir) if f.endswith('.db.bytes')) | |
| print(f"{'File':<45} {'Password':<45}") | |
| print("-" * 90) | |
| for fname in files: | |
| name = fname[:-len(".db.bytes")] | |
| pw = derive_password(name) | |
| print(f"{fname:<45} {pw:<45}") | |
| def main(): | |
| if len(sys.argv) >= 2 and sys.argv[1] == "list": | |
| db_dir = sys.argv[2] if len(sys.argv) > 2 else "DB" | |
| list_db_files(db_dir) | |
| return | |
| if len(sys.argv) >= 3: | |
| db_file = sys.argv[1] | |
| out_file = sys.argv[2] | |
| password = sys.argv[3] if len(sys.argv) > 3 else None | |
| pw, valid, msg = decrypt_file(db_file, out_file, password) | |
| print(f"Input: {db_file}") | |
| print(f"Output: {out_file}") | |
| print(f"Password: {pw}") | |
| print(f"Status: {msg}") | |
| return | |
| # Default: decrypt all files in DB/ | |
| script_dir = os.path.dirname(os.path.abspath(__file__)) | |
| db_dir = os.path.join(script_dir, "DB") | |
| output_dir = os.path.join(script_dir, "DB_decrypted") | |
| os.makedirs(output_dir, exist_ok=True) | |
| db_files = sorted(f for f in os.listdir(db_dir) if f.endswith('.db.bytes')) | |
| print(f"Decrypting {len(db_files)} database files...") | |
| print(f"Output: {output_dir}\n") | |
| print(f"{'File':<45} {'Password':<12} {'Status'}") | |
| print("-" * 75) | |
| success = 0 | |
| for fname in db_files: | |
| in_path = os.path.join(db_dir, fname) | |
| out_name = fname[:-len('.db.bytes')] + '.db' | |
| out_path = os.path.join(output_dir, out_name) | |
| try: | |
| pw, valid, msg = decrypt_file(in_path, out_path) | |
| status = "OK" if valid else f"FAILED: {msg}" | |
| if valid: | |
| success += 1 | |
| print(f"{fname:<45} {pw:<12} {status}") | |
| except Exception as e: | |
| print(f"{fname:<45} {'ERROR':<12} {e}") | |
| print(f"\nDone: {success}/{len(db_files)} succeeded") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment